Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Deploy] 쿠폰 필드 수정 사항 반영 배포 #152

Merged
merged 14 commits into from
Feb 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: build

on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Node.js 20.x
uses: actions/setup-node@v2
with:
node-version: 20.x

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false

- name: Install dependencies
run: pnpm install

- name: Run build
run: pnpm run build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ dist-ssr
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "HTTPS=true SSL_CRT_FILE=localhost.pem SSL_KEY_FILE=localhost-key.pem vite",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives",
"preview": "vite preview",
"postinstall": "husky install",
"cypress": "cypress open",
@@ -27,7 +27,9 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"react-toastify": "^10.0.4",
"typescript": "^4.9.4",
"typescript": "^5.7.3",
"wowds-icons": "^0.1.6",
"wowds-ui": "^0.2.0",
"zustand": "^4.4.7"
},
"devDependencies": {
11,001 changes: 5,221 additions & 5,780 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { toast } from "react-toastify";
import { BASE_URL } from "src/environment";
import { BASE_URL } from "@/environment";
import useAuthStorage from "@/hooks/auth/useAuthStorage";
import { ErrorResponse } from "@/types/entities/error";

const DEV_AUTH_TOKEN = import.meta.env.VITE_DEV_AUTH_TOKEN;

const createApiClient = (): AxiosInstance => {
const apiClient = axios.create({
baseURL: BASE_URL,
@@ -18,9 +20,8 @@ const createApiClient = (): AxiosInstance => {
const authStorage = useAuthStorage();

const accessToken = authStorage.accessToken;

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
if (accessToken || DEV_AUTH_TOKEN) {
config.headers.Authorization = `Bearer ${accessToken || DEV_AUTH_TOKEN}`;
}

return config;
9 changes: 9 additions & 0 deletions src/apis/studyListApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { apiClient } from ".";
import { StudyListApiResponseDtoType } from "@/types/dtos/study";

export const studyApi = {
getStudyList: async (): Promise<StudyListApiResponseDtoType[]> => {
const response = await apiClient.get("/admin/studies");
return response.data;
},
};
4 changes: 3 additions & 1 deletion src/components/AllMembers/AllMembersHeader.tsx
Original file line number Diff line number Diff line change
@@ -52,7 +52,9 @@ export default function AllMembersHeader() {

const handleChangeText = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const text = e.target.value;
text.length === 1 && handleResetPage();
if (text.length === 1) {
handleResetPage();
}

setSearchInfo(prevInfo => ({
...prevInfo,
11 changes: 11 additions & 0 deletions src/components/Coupon/CouponInfoTable.tsx
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ export default function CouponInfoTable() {
name: coupon.name,
discountAmount: formatPrice(coupon.discountAmount),
createdAt: formatDateWithText(coupon.createdAt),
couponType: coupon.couponType,
}));
};

@@ -76,6 +77,16 @@ const columns: GridColDef[] = [
resizable: false,
editable: false,
},
{
field: "couponType",
headerName: "쿠폰 종류",
headerAlign: "left",
align: "left",
minWidth: 200,
flex: 1,
resizable: false,
editable: false,
},
];

const StyledDataGrid = styled(DataGrid)({ border: "none", minHeight: 370 });
221 changes: 124 additions & 97 deletions src/components/Coupon/CouponModal.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,78 @@
import { ChangeEvent, useState } from "react";
import { ReactNode, useState } from "react";
import { ClassNames } from "@emotion/react";
import styled from "@emotion/styled";
import { Box, Button, Modal, TextField, Typography } from "@mui/material";
import { Box, Modal, Typography } from "@mui/material";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "react-toastify";
import Button from "wowds-ui/Button";
import DropDown from "wowds-ui/DropDown";
import DropDownOption from "wowds-ui/DropDownOption";
import TextField from "wowds-ui/TextField";
import { QueryKey } from "@/constants/queryKey";
import useCreateCouponMutation from "@/hooks/mutations/useCreateCouponMutation";
import useGetStudyListQuery from "@/hooks/queries/useGetStudyListQuery";
import { CouponInfoType } from "@/types/entities/coupon";

type CouponModalPropsType = {
open: boolean;
onClose: () => void;
};

type CouponInfoStateType = Omit<CouponInfoType, "couponType"> & {
couponType?: "ADMIN" | "STUDY_COMPLETION";
};

export default function CouponModal({ open, onClose }: CouponModalPropsType) {
const [couponInfo, setCouponInfo] = useState<CouponInfoType>({
const [couponInfo, setCouponInfo] = useState<CouponInfoStateType>({
name: "",
discountAmount: null,
couponType: null as unknown as CouponInfoType["couponType"],
studyId: null,
});

const { mutate: createCouponMutate } = useCreateCouponMutation();

const queryClient = useQueryClient();

const handleChangeCouponInfo = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value, name } = e.target;
const studyList = useGetStudyListQuery();

const { mutate: createCouponMutate } = useCreateCouponMutation();

const isCouponInfoComplete =
couponInfo.name !== null &&
couponInfo.name !== "" &&
couponInfo.discountAmount !== null &&
couponInfo.couponType !== null &&
(couponInfo.couponType === "ADMIN" || couponInfo.studyId !== null);

const handleChangeName = (value: string) => {
setCouponInfo(prevCouponInfo => ({
...prevCouponInfo,
name: value,
}));
};
const handleChangeDiscountAmount = (value: string) => {
setCouponInfo(prevCouponInfo => ({
...prevCouponInfo,
[name]: value,
discountAmount: Number(value),
}));
};

const handleClickSubmit = () => {
const { name, discountAmount } = couponInfo;
const handleChangeCouponType = (value: { selectedValue: string; selectedText: ReactNode }) => {
setCouponInfo(prevCouponInfo => ({
...prevCouponInfo,
couponType: value.selectedValue as CouponInfoType["couponType"],
studyId: value.selectedValue === "ADMIN" ? null : prevCouponInfo.studyId,
}));
};

if (!name || !discountAmount) {
toast.error(`채워지지 않는 필드가 있어요. 모든 필드를 채워주세요!`);
return;
}
const handleChangeStudyId = (value: { selectedValue: string; selectedText: ReactNode }) => {
setCouponInfo(prevCouponInfo => ({
...prevCouponInfo,
studyId: Number(value.selectedValue) as CouponInfoType["studyId"],
}));
};

createCouponMutate(couponInfo, {
const handleClickSubmit = () => {
createCouponMutate(couponInfo as CouponInfoType, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QueryKey.couponList],
@@ -53,33 +86,74 @@ export default function CouponModal({ open, onClose }: CouponModalPropsType) {
return (
<Modal open={open} onClose={onClose}>
<StyledModalContentWrapper>
<StyledTitle>{"쿠폰 생성"}</StyledTitle>
<StyledTitle>쿠폰 생성</StyledTitle>
<StyledContent>
<StyledInfoRow>
<StyledInfoWrapper>
<StyledText>이름</StyledText>
<StyledTextField
placeholder="이름"
size="small"
value={couponInfo.name}
name="name"
onChange={handleChangeCouponInfo}
/>
</StyledInfoWrapper>
<StyledInfoWrapper>
<StyledText>할인금액</StyledText>
<StyledTextField
placeholder="금액"
size="small"
value={couponInfo.discountAmount}
name="discountAmount"
onChange={handleChangeCouponInfo}
/>
</StyledInfoWrapper>
</StyledInfoRow>
<StyledButton size="large" variant="contained" onClick={handleClickSubmit}>
{"생성하기"}
</StyledButton>
<StyledInfoWrapper>
<TextField
label="이름"
placeholder="이름"
value={couponInfo.name}
onChange={handleChangeName}
/>
<TextField
label="할인 금액"
placeholder="금액"
value={couponInfo.discountAmount === null ? "" : String(couponInfo.discountAmount)}
onChange={handleChangeDiscountAmount}
/>

<ClassNames>
{({ css }) => {
const dropdownClass = css`
width: 22.375rem !important;
align-items: flex-start;
& > button {
width: 100%;
}
`;

return (
<StyledInfoRow>
<DropDown
label="쿠폰 종류"
placeholder="선택하세요"
value={couponInfo.couponType}
onChange={handleChangeCouponType}
className={dropdownClass}
>
<DropDownOption value="ADMIN" text="어드민" />
<DropDownOption value="STUDY_COMPLETION" text="스터디 수료" />
</DropDown>

{couponInfo.couponType === "STUDY_COMPLETION" && (
<DropDown
label="스터디 이름"
placeholder="선택하세요"
value={String(couponInfo.studyId)}
onChange={handleChangeStudyId}
className={dropdownClass}
>
{studyList.map(study => (
<DropDownOption
key={study.studyId}
value={String(study.studyId)}
text={study.title}
/>
))}
</DropDown>
)}
</StyledInfoRow>
);
}}
</ClassNames>
</StyledInfoWrapper>
<Button
disabled={!isCouponInfoComplete}
style={{ width: "20.5rem" }}
onClick={handleClickSubmit}
>
생성하기
</Button>
</StyledContent>
</StyledModalContentWrapper>
</Modal>
@@ -108,27 +182,20 @@ const StyledTitle = styled(Typography)({
letterSpacing: "-0.32px",
});

const StyledText = styled(Typography)({
color: "#6B6B6B",
fontFamily: "SUIT v1",
fontSize: "14px",
fontWeight: 600,
lineHeight: "14px",
letterSpacing: "-0.14px",
});

const StyledInfoRow = styled(Box)({
const StyledInfoRow = styled("div")({
display: "flex",
gap: "10px",
flex: 1,
});

const StyledContent = styled(Box)({
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "space-between",
height: "490px",
"display": "flex",
"flexDirection": "column",
"alignItems": "flex-start",
"justifyContent": "space-between",
"height": "490px",
"& > :last-child": {
alignSelf: "center",
},
});

const StyledInfoWrapper = styled(Box)<{ height?: number }>(({ height }) => ({
@@ -137,44 +204,4 @@ const StyledInfoWrapper = styled(Box)<{ height?: number }>(({ height }) => ({
flexDirection: "column",
alignItems: "flex-start",
gap: "8px",
flex: 1,
}));

const StyledTextField = styled(TextField)({
"width": "274px",

".MuiInputBase-root": {
borderRadius: 4,
border: "1px solid #C2C2C2",
padding: "8px 14px",
height: "40px",
},

".MuiInputBase-input": {
padding: 0,
fontFamily: "Pretendard",
fontSize: "14px",
fontWeight: 500,
lineHeight: "22.4px",
},

"fieldset": {
border: "none",
},

".MuiInputBase-input::placeholder": {
color: "#C2C2C2",
},
});

const StyledButton = styled(Button)({
width: "328px",
height: "48px",
padding: "16px 0",
marginTop: "auto",
fontFamily: "SUIT v1",
fontSize: "16px",
fontWeight: 600,
lineHeight: "16px",
letterSpacing: "-0.16px",
});
4 changes: 3 additions & 1 deletion src/components/CouponProvision/CouponProvisionHeader.tsx
Original file line number Diff line number Diff line change
@@ -65,7 +65,9 @@ export default function CouponProvisionHeader() {

const handleChangeText = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const text = e.target.value;
text.length === 1 && handleResetPage();
if (text.length === 1) {
handleResetPage();
}

setSearchInfo(prevSearchInfo => ({
...prevSearchInfo,
4 changes: 3 additions & 1 deletion src/components/IssuedCoupon/IssuedCouponHeader.tsx
Original file line number Diff line number Diff line change
@@ -65,7 +65,9 @@ export default function IssuedCouponHeader() {

const handleChangeText = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const text = e.target.value;
text.length === 1 && handleResetPage();
if (text.length === 1) {
handleResetPage();
}

setSearchInfo(prevSearchInfo => ({
...prevSearchInfo,
4 changes: 3 additions & 1 deletion src/components/PaymentStatus/PaymentStatusHeader.tsx
Original file line number Diff line number Diff line change
@@ -48,7 +48,9 @@ export default function PaymentStatusHeader() {

const handleChangeText = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const text = e.target.value;
text.length === 1 && handleResetPage();
if (text.length === 1) {
handleResetPage();
}

setSearchInfo(prevSearchInfo => ({
...prevSearchInfo,
4 changes: 3 additions & 1 deletion src/components/PendingMembers/PendingMembersHeader.tsx
Original file line number Diff line number Diff line change
@@ -54,7 +54,9 @@ export default function PendingMembersHeader() {

const handleChangeText = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const text = e.target.value;
text.length === 1 && handleResetPage();
if (text.length === 1) {
handleResetPage();
}

setSearchInfo(prevSearchInfo => ({
...prevSearchInfo,
3 changes: 2 additions & 1 deletion src/constants/queryKey.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,11 @@ export const enum QueryKey {
pendingMemberList = "pendingMemberList",
departmentList = "departmentList",
paymentList = "paymentList",
paymentDetailInfo = 'paymentDetailInfo',
paymentDetailInfo = "paymentDetailInfo",
couponList = "couponList",
couponProvisionMemberList = "couponProvisionMemberList",
issuedCouponList = "issuedCouponList",
recruitment = "recruitment",
recruitmentRound = "recruitmentRound",
studyList = "studyList",
}
18 changes: 18 additions & 0 deletions src/hooks/queries/useGetStudyListQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { studyApi } from "@/apis/studyListApi";
import { QueryKey } from "@/constants/queryKey";

export default function useGetStudyListQuery() {
const { data = [] } = useQuery({
queryKey: [QueryKey.studyList],
queryFn: () => studyApi.getStudyList(),
});

return data
.map(study => ({
studyId: study.studyId,
title: study.title,
openingDate: new Date(study.openingDate),
}))
.sort((a, b) => b.openingDate.getTime() - a.openingDate.getTime());
}
3 changes: 2 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "src/App";
import App from "./App";
import "styles/global.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
4 changes: 3 additions & 1 deletion src/pages/AuthServerRedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Navigate } from "react-router-dom";
import RoutePath from "@/routes/routePath";

export const AuthServerRedirectPage = () => {
const AuthServerRedirectPage = () => {
sessionStorage.setItem("isLogin", "true");

return <Navigate to={RoutePath.Index} />;
};

export default AuthServerRedirectPage;
2 changes: 1 addition & 1 deletion src/routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import Layout from "@/components/@layout/Layout";
import AllMemberHistoryPerSemesterPage from "@/pages/AllMemberHistoryPerSemesterPage";
import AllMembersPage from "@/pages/AllMembersPage";
import AuthErrorPage from "@/pages/AuthErrorPage";
import { AuthServerRedirectPage } from "@/pages/AuthServerRedirectPage";
import AuthServerRedirectPage from "@/pages/AuthServerRedirectPage";
import CouponPage from "@/pages/CouponPage";
import CouponProvisionPage from "@/pages/CouponProvisionPage";
import IssuedCouponPage from "@/pages/IssuedCouponPage";
1 change: 1 addition & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import url("wowds-ui/styles.css");
18 changes: 18 additions & 0 deletions src/types/dtos/study.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DayOfWeekType } from "../entities/dayofWeek";
import { StudyKoreanType, StudySemesterType } from "../entities/study";
import { TimeType } from "../entities/time";

export interface StudyListApiResponseDtoType {
studyId: number;
academicYear: number;
semesterType: StudySemesterType;
title: string;
studyType: StudyKoreanType;
notionLink: string;
introduction: string;
mentorName: string;
dayOfWeek: DayOfWeekType;
startTime: TimeType;
totalWeek: number;
openingDate: string;
}
5 changes: 5 additions & 0 deletions src/types/entities/coupon.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { MemberInfoType } from "./member";

export type CouponTypeType = "ADMIN" | "STUDY_COMPLETION";

export type CouponInfoType = {
name: string;
discountAmount: null | number;
couponType: CouponTypeType;
studyId: null | number;
};

export type DetailCouponInfoType = {
@@ -38,6 +42,7 @@ export type CouponType = {
name: string;
discountAmount: number;
createdAt: string;
couponType: CouponTypeType;
};

type CouponSearchVariantType = [
8 changes: 8 additions & 0 deletions src/types/entities/dayofWeek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type DayOfWeekType =
| "MONDAY"
| "TUESDAY"
| "WEDNESDAY"
| "THURSDAY"
| "FRIDAY"
| "SATURDAY"
| "SUNDAY";
27 changes: 27 additions & 0 deletions src/types/entities/study.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type StudyDifficultyArrayType = {
text: string;
value: StudyDifficultyType;
}[];

export type StudyAssignmentStatusType = "NONE" | "OPEN" | "CANCELED";

export type StudyDifficultyType = "HIGH" | "MEDIUM" | "LOW" | "BASIC";

export type StudyType = "ASSIGNMENT" | "ONLINE" | "OFFLINE";

export type StudyKoreanType = "과제 스터디" | "온라인 스터디" | "오프라인 스터디";

export type StudyCurriculumType = {
studyDetailId: number;
title?: string;
description?: string;
difficulty?: StudyDifficultyType;
status?: StudyAssignmentStatusType;
};

export type StudyAnnouncementType = {
title: string;
link: string;
};

export type StudySemesterType = "FIRST" | "SECOND";
6 changes: 6 additions & 0 deletions src/types/entities/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type TimeType = {
hour: number;
minute: number;
second: number;
nano: number;
};
31 changes: 16 additions & 15 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
"module": "ESNext",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
@@ -19,23 +19,24 @@
"noFallthroughCasesInSwitch": true,
"jsxImportSource": "@emotion/react",
/* 절대 경로 */
"baseUrl": ".",
"baseUrl": "src",
"paths": {
"@/*": ["src/*"],
"apis/*": ["src/apis/*"],
"assets/*": ["src/assets/*"],
"components/*": ["src/components/*"],
"constants/*": ["src/constants/*"],
"hooks/*": ["src/hooks/*"],
"pages/*": ["src/pages/*"],
"store/*": ["src/store/*"],
"styles/*": ["src/styles/*"],
"tests/*": ["src/__tests__/*"],
"types/*": ["src/types/*"],
"utils/*": ["src/utils/*"],
"routes/*": ["src/routes/*"]
"@/*": ["*"],
"apis/*": ["apis/*"],
"assets/*": ["assets/*"],
"components/*": ["components/*"],
"constants/*": ["constants/*"],
"hooks/*": ["hooks/*"],
"pages/*": ["pages/*"],
"store/*": ["store/*"],
"styles/*": ["styles/*"],
"tests/*": ["__tests__/*"],
"types/*": ["types/*"],
"utils/*": ["utils/*"],
"routes/*": ["routes/*"]
}
},
"include": ["src", "**/*.*"],
"exclude": ["vite.config.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}
2 changes: 1 addition & 1 deletion tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -6,4 +6,7 @@ import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths(), svgr()],
server: {
port: 5174,
},
});