diff --git a/README.md b/README.md index 438763d5..a0b4f595 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,17 @@ -# Turborepo Tailwind CSS starter - -This Turborepo starter is maintained by the Turborepo core team. - -## Using this example - -Run the following command: - -```sh -npx create-turbo@latest -e with-tailwind -``` - -## What's inside? - -This Turborepo includes the following packages/apps: - -### Apps and Packages - -- `docs`: a [Next.js](https://nextjs.org/) app with [Tailwind CSS](https://tailwindcss.com/) -- `web`: another [Next.js](https://nextjs.org/) app with [Tailwind CSS](https://tailwindcss.com/) -- `ui`: a stub React component library with [Tailwind CSS](https://tailwindcss.com/) shared by both `web` and `docs` applications -- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo - -Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). - -### Building packages/ui - -This example is set up to produce compiled styles for `ui` components into the `dist` directory. The component `.tsx` files are consumed by the Next.js apps directly using `transpilePackages` in `next.config.ts`. This was chosen for several reasons: - -- Make sharing one `tailwind.config.ts` to apps and packages as easy as possible. -- Make package compilation simple by only depending on the Next.js Compiler and `tailwindcss`. -- Ensure Tailwind classes do not overwrite each other. The `ui` package uses a `ui-` prefix for it's classes. -- Maintain clear package export boundaries. - -Another option is to consume `packages/ui` directly from source without building. If using this option, you will need to update the `tailwind.config.ts` in your apps to be aware of your package locations, so it can find all usages of the `tailwindcss` class names for CSS compilation. - -For example, in [tailwind.config.ts](packages/tailwind-config/tailwind.config.ts): - -```js - content: [ - // app content - `src/**/*.{js,ts,jsx,tsx}`, - // include packages if not transpiling - "../../packages/ui/*.{js,ts,jsx,tsx}", - ], -``` - -If you choose this strategy, you can remove the `tailwindcss` and `autoprefixer` dependencies from the `ui` package. - -### Utilities - -This Turborepo has some additional tools already setup for you: - -- [Tailwind CSS](https://tailwindcss.com/) for styles -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting +# GSMC-front +GSM 인증제 관리 서비스 GSMC 입니다! + +## Tech Stack +- **Next.js - V15** +- **React - V19** +- **TypeScript** +- **Tailwind CSS - V3** +- **Axios** +- **Tanstack-query** +- **React-Hook-Form** +- **ESLint** +- **Prettier** + +### Architecture +- **Turborepo(Monorepo)** +- **FSD Architecture(Feature Sliced Design)** diff --git a/apps/admin/src/app/change-password/page.tsx b/apps/admin/src/app/change-password/page.tsx deleted file mode 100644 index 974e59a5..00000000 --- a/apps/admin/src/app/change-password/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ChangePasswordView from "@/views/change-password"; - -const ChangePasswordPage = () => { - return ; -}; - -export default ChangePasswordPage; diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 1bc9dfc4..cebf0264 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -1,12 +1,13 @@ +import Header from "@/widgets/header/ui"; import { PostProvider } from "@repo/store/postProvider"; import type { Metadata } from "next"; import { Toaster } from "sonner"; import "./globals.css"; -import { MemberProvider } from "@/entities/member/model/memberContext"; - import Providers from "./providers"; +import { MemberProvider } from "@/entities/member/model/memberContext"; + export const metadata: Metadata = { title: "GSMC", description: @@ -23,7 +24,10 @@ export default function RootLayout({ - {children} + +
+ {children} + diff --git a/apps/admin/src/entities/check-post/api/changeEvidenceState.ts b/apps/admin/src/entities/check-post/api/changeEvidenceState.ts index 9949a3b4..97862339 100644 --- a/apps/admin/src/entities/check-post/api/changeEvidenceState.ts +++ b/apps/admin/src/entities/check-post/api/changeEvidenceState.ts @@ -1,5 +1,5 @@ import instance from "@repo/api/axios"; -import type { postState } from "@repo/types/evidences"; +import type { PostStatus } from "@repo/types/evidences"; interface ChangeEvidenceStateResponse { status: number; @@ -8,11 +8,11 @@ interface ChangeEvidenceStateResponse { export const changeEvidenceState = async ( evidenceId: number, - status: postState, + status: PostStatus ): Promise => { const response = await instance.patch( `/evidence/${evidenceId}/status`, - { status }, + { status } ); return response; }; diff --git a/apps/admin/src/entities/check-post/api/getStudent.ts b/apps/admin/src/entities/check-post/api/getStudent.ts deleted file mode 100644 index 8ccd7189..00000000 --- a/apps/admin/src/entities/check-post/api/getStudent.ts +++ /dev/null @@ -1,14 +0,0 @@ -import instance from "@repo/api/axios"; -import type { Member } from "@repo/types/member"; -import { type AxiosResponse } from "axios"; - -export const getStudent = async ( - email: string, -): Promise> => { - const id = email.split("@")[0]?.slice(1); - try { - return await instance.get(`members/students/${id}`); - } catch { - throw new Error("학생 정보를 불러오는 데 실패했습니다."); - } -}; diff --git a/apps/admin/src/entities/check-post/ui/post.tsx b/apps/admin/src/entities/check-post/ui/post.tsx index 8b09f444..ca3c82a5 100644 --- a/apps/admin/src/entities/check-post/ui/post.tsx +++ b/apps/admin/src/entities/check-post/ui/post.tsx @@ -1,47 +1,61 @@ -"use client"; - -import type { post } from "@repo/types/evidences"; +import type { PostType } from "@repo/types/evidences"; import { getCategoryName } from "@repo/utils/handleCategory"; import { isActivity, isOthers, isReading } from "@repo/utils/handlePost"; import { handleState, handleStateColor } from "@repo/utils/handleState"; import Image from "next/image"; interface PostProps { - data: post; + data: PostType; onClick?: () => void; } const Post = ({ data, onClick }: PostProps) => { + + const title = (() => { + if ("title" in data) return data.title; + if ("evidenceType" in data) return data.evidenceType + return null; + })(); + + const imageUri = isActivity(data) ? data.imageUri : null; + + const state = "status" in data ? data.status : null; + + const subTitle = (() => { + if (isReading(data)) return `${data.author}`; + if (isOthers(data)) return getCategoryName(data.categoryName); + if (isActivity(data)) return getCategoryName(data.categoryName); + return null; + })(); + return (
-
- {isActivity(data) && data.imageUri != null ? ( +
+ {imageUri == null ? null : ( {data.title} - ) : null} + )}
+
-

- {isActivity(data) || isReading(data) ? data.title : null} +

+ {title}

-

- {isActivity(data) || isOthers(data) - ? getCategoryName(data.categoryName) - : ("author" in data - ? data.author - : "")} +

+ {subTitle}

- - {handleState(data.status)} - + {state ? + + {handleState(state)} + : null}
); diff --git a/apps/admin/src/entities/member/api/getScore.ts b/apps/admin/src/entities/member/api/getScore.ts new file mode 100644 index 00000000..10df9ff7 --- /dev/null +++ b/apps/admin/src/entities/member/api/getScore.ts @@ -0,0 +1,16 @@ +import instance from "@repo/api"; +import { isAxiosError } from "axios"; + +import type { ScoreResponse } from "../model/score"; + +export const getScore = async (id: string): Promise => { + try { + const res = await instance.get(`/score/${id}`); + return res.data; + } catch (error: unknown) { + if (isAxiosError(error) && error.response) { + throw error.response.data ?? "점수 정보를 불러오는데 실패했습니다"; + } + throw error; + } +}; diff --git a/apps/admin/src/entities/member/model/score.ts b/apps/admin/src/entities/member/model/score.ts new file mode 100644 index 00000000..8aa3f905 --- /dev/null +++ b/apps/admin/src/entities/member/model/score.ts @@ -0,0 +1,10 @@ +export interface ScoreResponse { + totalScore: number; + scores: Score[]; +} + +export interface Score { + categoryName: string; + value: number; + convertedValue: number; +} diff --git a/apps/admin/src/entities/member/model/useGetScore.ts b/apps/admin/src/entities/member/model/useGetScore.ts new file mode 100644 index 00000000..8966498c --- /dev/null +++ b/apps/admin/src/entities/member/model/useGetScore.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; + +import { getScore } from "../api/getScore"; + +import type { ScoreResponse } from "./score"; + +export const useGetScore = (id: string) => { + return useQuery({ + queryKey: ["score", id], + queryFn: () => getScore(id), + enabled: !!id, + }); +}; diff --git a/apps/admin/src/entities/member/ui/scoreModal.tsx b/apps/admin/src/entities/member/ui/scoreModal.tsx new file mode 100644 index 00000000..058a105d --- /dev/null +++ b/apps/admin/src/entities/member/ui/scoreModal.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Button } from "@repo/shared/button"; +import Card from "@repo/shared/card"; +import List from "@repo/shared/list"; +import { getCategoryName } from "@repo/utils/handleCategory"; + +import { useGetScore } from "../model/useGetScore"; + +interface ScoreModalProps { + close: () => void; + show: boolean; + id: string; +} + +export default function ScoreModal({ show, close, id }: ScoreModalProps) { + const { data } = useGetScore(id); + if (!show) return null; + + return ( +
+
+

+ 부분점수 +

+ + {data?.scores.map((v) => ( + + ))} + +
+
+ ); +} diff --git a/apps/admin/src/entities/signin/api/postSignin.ts b/apps/admin/src/entities/signin/api/postSignin.ts index fcdc816b..6029fdc0 100644 --- a/apps/admin/src/entities/signin/api/postSignin.ts +++ b/apps/admin/src/entities/signin/api/postSignin.ts @@ -1,33 +1,21 @@ import instance from "@repo/api/axios"; import { isAxiosError, type AxiosResponse } from "axios"; -import type { SigninFormProps } from "@/shared/model/AuthForm"; - -interface SigninResponse { - accessToken: string; - refreshToken: string; -} +import type { + SigninFormProps, + SigninFormResponse, +} from "@/shared/model/signin"; export const postSignin = async ( - form: SigninFormProps, -): Promise => { + form: SigninFormProps +): Promise> => { try { - const response: AxiosResponse = await instance.post( - `/auth/signin`, - form, - ); - return response.data; - } catch (error: unknown) { + const response = await instance.post(`/auth/signin`, form); + return response; + } catch (error) { if (isAxiosError(error) && error.response) { - const message = - typeof error.response.data === "object" && - error.response.data !== null && - "message" in error.response.data - ? (error.response.data as { message?: string }).message - : undefined; - - throw new Error(message ?? "로그인 실패"); + throw error.response.data ?? "로그인 실패"; } - throw new Error("알 수 없는 로그인 에러 발생"); + throw error; } }; diff --git a/apps/admin/src/entities/signup/api/postSignup.ts b/apps/admin/src/entities/signup/api/postSignup.ts index 420a12a1..a54e35a0 100644 --- a/apps/admin/src/entities/signup/api/postSignup.ts +++ b/apps/admin/src/entities/signup/api/postSignup.ts @@ -1,20 +1,15 @@ import instance from "@repo/api/axios"; -import { isAxiosError } from "axios"; +import { isAxiosError, type AxiosResponse } from "axios"; -import type { SignupFormProps } from "@/shared/model/AuthForm"; - -interface SignupResponse { - success: boolean; - message: string; -} +import type { SignupFormProps } from "@/shared/model/signup"; export const postSignup = async ( - form: SignupFormProps, -): Promise => { + form: SignupFormProps +): Promise => { try { - const response = await instance.post(`/auth/signup`, form); - return response.data; - } catch (error: unknown) { + const response = await instance.post(`/auth/signup`, form); + return response; + } catch (error) { if (isAxiosError(error) && error.response) { throw error.response.data ?? "회원가입 실패"; } diff --git a/apps/admin/src/shared/api/getMember.ts b/apps/admin/src/shared/api/getMember.ts new file mode 100644 index 00000000..8db274e4 --- /dev/null +++ b/apps/admin/src/shared/api/getMember.ts @@ -0,0 +1,22 @@ +import instance from "@repo/api/axios"; +import type { Member } from "@repo/types/member"; +import { isAxiosError, type AxiosResponse } from "axios"; + +import { extractStudentCode } from "@/shared/util/extractStudentId"; + +export const getMember = async ( + email: string +): Promise> => { + const studentCode = extractStudentCode(email); + try { + const respone = await instance.get( + `/members/students/${studentCode}` + ); + return respone; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw error.response.data ?? "학생 정보를 불러오지 못했습니다."; + } + throw error; + } +}; diff --git a/apps/admin/src/shared/api/getMembers.ts b/apps/admin/src/shared/api/getMembers.ts new file mode 100644 index 00000000..835c4e2b --- /dev/null +++ b/apps/admin/src/shared/api/getMembers.ts @@ -0,0 +1,15 @@ +import instance from "@repo/api/axios"; +import type { Member } from "@repo/types/member"; +import { isAxiosError, type AxiosResponse } from "axios"; + +export const getMembers = async (): Promise> => { + try { + const respone = await instance.get("/members/students"); + return respone; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw error.response.data ?? "학생 목록을 불러오지 못했습니다."; + } + throw error; + } +}; diff --git a/apps/admin/src/shared/api/patchPassword.ts b/apps/admin/src/shared/api/patchPassword.ts index 0092acec..601682e3 100644 --- a/apps/admin/src/shared/api/patchPassword.ts +++ b/apps/admin/src/shared/api/patchPassword.ts @@ -1,13 +1,13 @@ import instance from "@repo/api/axios"; import { isAxiosError } from "axios"; -import type { ChangePasswordProps } from "../model/changePWForm"; +import type { ChangePasswordProps } from "../model/changePassword"; export const patchPassword = async (form: ChangePasswordProps) => { try { const response = await instance.patch<{ success: boolean }>( `/auth/change-password`, - form, + form ); return response.data; diff --git a/apps/admin/src/shared/model/AuthForm.ts b/apps/admin/src/shared/model/AuthForm.ts deleted file mode 100644 index c3b0f5f5..00000000 --- a/apps/admin/src/shared/model/AuthForm.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { role } from "@repo/types/member"; - -export interface SigninFormProps { - email: string; - password: string; -} - -export interface SigninFormResponse { - accessToken: string; - accessTokenExpiresAt: Date; - refreshToken: string; - refreshTokenExpiresAt: Date; - role: role; -} - -export interface AuthStepForm { - email: string; - authcode: string; - name: string; -} - -export interface SignupStepForm { - password: string; - passwordCheck: string; -} - -export interface SignupFormProps { - email: string; - password: string; - name: string; -} - -export interface ServerResponse { - success: boolean; - message?: string; -} diff --git a/apps/admin/src/shared/model/changePWForm.ts b/apps/admin/src/shared/model/changePWForm.ts deleted file mode 100644 index e22531d0..00000000 --- a/apps/admin/src/shared/model/changePWForm.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ChangePasswordProps { - email: string; - password: string; -} - -export interface ChangePasswordForm extends ChangePasswordProps { - passwordCheck: string; -} diff --git a/apps/admin/src/shared/model/changePassword.ts b/apps/admin/src/shared/model/changePassword.ts new file mode 100644 index 00000000..6c76533c --- /dev/null +++ b/apps/admin/src/shared/model/changePassword.ts @@ -0,0 +1,8 @@ +import type { StepAuthCodeForm } from "./signup"; + +export interface ChangePasswordProps { + email: string; + password: string; +} + +export type ChangePassword_StepAuthCodeForm = Omit; diff --git a/apps/admin/src/shared/types/error.ts b/apps/admin/src/shared/model/error.ts similarity index 100% rename from apps/admin/src/shared/types/error.ts rename to apps/admin/src/shared/model/error.ts diff --git a/apps/admin/src/shared/model/signin.ts b/apps/admin/src/shared/model/signin.ts new file mode 100644 index 00000000..cb9c8410 --- /dev/null +++ b/apps/admin/src/shared/model/signin.ts @@ -0,0 +1,14 @@ +import type { role } from "@repo/types/member"; + +export interface SigninFormProps { + email: string; + password: string; +} + +export interface SigninFormResponse { + accessToken: string; + accessTokenExpiresAt: Date; + refreshToken: string; + refreshTokenExpiresAt: Date; + role: role; +} diff --git a/apps/admin/src/shared/model/signup.ts b/apps/admin/src/shared/model/signup.ts new file mode 100644 index 00000000..90ea1090 --- /dev/null +++ b/apps/admin/src/shared/model/signup.ts @@ -0,0 +1,15 @@ +export interface SignupFormProps { + email: string; + password: string; + name: string; +} +export interface StepAuthCodeForm { + email: string; + authcode: string; + name: string; +} + +export interface StepPasswordForm { + password: string; + passwordCheck: string; +} diff --git a/apps/admin/src/entities/check-post/model/useGetStudent.ts b/apps/admin/src/shared/model/useGetMember.ts similarity index 68% rename from apps/admin/src/entities/check-post/model/useGetStudent.ts rename to apps/admin/src/shared/model/useGetMember.ts index 845b51a7..045326cc 100644 --- a/apps/admin/src/entities/check-post/model/useGetStudent.ts +++ b/apps/admin/src/shared/model/useGetMember.ts @@ -2,12 +2,12 @@ import type { Member } from "@repo/types/member"; import { useQuery } from "@tanstack/react-query"; import type { AxiosResponse } from "axios"; -import { getStudent } from "../api/getStudent"; +import { getMember } from "../api/getMember"; -export const useGetStudent = (email: string) => { +export const useGetMember = (email: string) => { return useQuery>({ queryKey: ["student", email], - queryFn: () => getStudent(email), + queryFn: () => getMember(email), staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 30, }); diff --git a/apps/admin/src/shared/model/useGetMembers.ts b/apps/admin/src/shared/model/useGetMembers.ts new file mode 100644 index 00000000..954b4158 --- /dev/null +++ b/apps/admin/src/shared/model/useGetMembers.ts @@ -0,0 +1,12 @@ +import type { Member } from "@repo/types/member"; +import { useQuery } from "@tanstack/react-query"; +import type { AxiosResponse } from "axios"; + +import { getMembers } from "../api/getMembers"; + +export const useGetMembers = () => { + return useQuery>({ + queryKey: ["members"], + queryFn: getMembers, + }); +}; diff --git a/apps/admin/src/shared/ui/header.tsx b/apps/admin/src/shared/ui/header.tsx deleted file mode 100644 index 2e42503b..00000000 --- a/apps/admin/src/shared/ui/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import TextLogo from "@repo/shared/textLogo"; -import { deleteCookie } from "@repo/utils/deleteCookie"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useCallback } from "react"; - -const Header = () => { - const router = useRouter(); - - const signout = useCallback(() => { - deleteCookie("accessToken"); - deleteCookie("refreshToken"); - router.push("/signin"); - }, [router]); - return ( -
- - - - -
- - 비밀번호 변경 - -
- 로그아웃 -
-
-
- ); -}; - -export default Header; diff --git a/apps/admin/src/shared/util/extractStudentId.ts b/apps/admin/src/shared/util/extractStudentId.ts new file mode 100644 index 00000000..883bfaba --- /dev/null +++ b/apps/admin/src/shared/util/extractStudentId.ts @@ -0,0 +1,4 @@ +export const extractStudentCode = (email: string): string | null => { + const match = /^s(\d{5})@gsm\.hs\.kr$/.exec(email); + return match && typeof match[1] === "string" ? match[1] : null; +}; diff --git a/apps/admin/src/views/change-password/index.tsx b/apps/admin/src/views/change-password/index.tsx deleted file mode 100644 index 9452e6b9..00000000 --- a/apps/admin/src/views/change-password/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import { Button } from "@repo/shared/button"; -import { useMutation } from "@tanstack/react-query"; -import { useRouter } from "next/navigation"; -import { useCallback } from "react"; -import { useForm } from "react-hook-form"; - -import { patchPassword } from "@/shared/api/patchPassword"; -import type { ServerResponse } from "@/shared/model/AuthForm"; -import type { - ChangePasswordForm, - ChangePasswordProps, -} from "@/shared/model/changePWForm"; -import type { HttpError } from "@/shared/types/error"; -import ChangePassword from "@/widgets/changePassword/ui"; -import { AuthForm } from "@widgets/auth/ui"; - -const ChangePasswordView = () => { - const router = useRouter(); - - const { mutate: changePWMutate } = useMutation< - ServerResponse, - HttpError, - ChangePasswordProps - >({ - mutationFn: patchPassword, - onSuccess: (data) => { - if (data.success) router.push("/"); - }, - }); - - const { - control, - handleSubmit, - formState: { isValid }, - } = useForm({ - mode: "onChange", - defaultValues: { email: "", password: "", passwordCheck: "" }, - }); - - const onSubmit = useCallback( - (form: ChangePasswordProps) => { - changePWMutate({ - email: form.email, - password: form.password, - }); - }, - [changePWMutate], - ); - - const handleChangePassword = useCallback( - (e: React.FormEvent) => { - void handleSubmit(onSubmit)(e); - }, - [handleSubmit, onSubmit], - ); - - return ( -
- -
-
- -
-
- ); -}; - -export default ChangePasswordView; diff --git a/apps/admin/src/views/check-post/api/getPosts.ts b/apps/admin/src/views/check-post/api/getPosts.ts index b895ef4a..1f24edde 100644 --- a/apps/admin/src/views/check-post/api/getPosts.ts +++ b/apps/admin/src/views/check-post/api/getPosts.ts @@ -1,12 +1,22 @@ import instance from "@repo/api/axios"; -import type { EvidenceResponse, postState } from "@repo/types/evidences"; -import type { AxiosResponse } from "axios"; +import type { PostResponse, PostStatus } from "@repo/types/evidences"; +import { isAxiosError } from "axios"; -export const getPosts = async ( - email: string, - status: postState | null, -): Promise> => { - return await instance.get( - `/evidence/${decodeURIComponent(email).split("@")[0]?.slice(1)}?status=${status}`, - ); +import { extractStudentCode } from "@/shared/util/extractStudentId"; + +export const getPosts = async (email: string, status: PostStatus) => { + const studentCode = extractStudentCode(email); + try { + const response = await instance.get( + `/evidence/${studentCode}?status=${status}` + ); + return response; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw ( + error.response.data ?? "학생 게시글 정보를 불러오는 데 실패했습니다." + ); + } + throw error; + } }; diff --git a/apps/admin/src/views/check-post/model/useChangeEvidenceState.ts b/apps/admin/src/views/check-post/model/useChangeEvidenceState.ts deleted file mode 100644 index 0f6f36de..00000000 --- a/apps/admin/src/views/check-post/model/useChangeEvidenceState.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { postState } from "@repo/types/evidences"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback } from "react"; -import { toast } from "sonner"; - -import { changeEvidenceState } from "@/entities/check-post/api/changeEvidenceState"; - -export function useChangeEvidenceState(postId: number) { - const queryClient = useQueryClient(); - - const updatePostState = useCallback( - async (state: postState) => { - try { - if (postId) { - const res = await changeEvidenceState(postId, state); - - if (res.status === 204) { - toast.success("게시글 상태가 변경되었습니다."); - - // posts 쿼리만 무효화 - await queryClient.invalidateQueries({ - queryKey: ["posts"], - exact: false, - }); - } - } - } catch { - toast.error("게시글 상태 변경에 실패했습니다."); - } - }, - [postId, queryClient], - ); - - const handlePostState = useCallback( - (state: postState) => (e: React.MouseEvent) => { - e.stopPropagation(); - void updatePostState(state); - }, - [updatePostState], - ); - - return { handlePostState }; -} diff --git a/apps/admin/src/views/check-post/model/useChangeEvidenceStatus.ts b/apps/admin/src/views/check-post/model/useChangeEvidenceStatus.ts new file mode 100644 index 00000000..97884589 --- /dev/null +++ b/apps/admin/src/views/check-post/model/useChangeEvidenceStatus.ts @@ -0,0 +1,46 @@ +"use client"; + +import type { PostStatus } from "@repo/types/evidences"; +import { useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { useCallback } from "react"; +import { toast } from "sonner"; + +import { changeEvidenceState } from "@/entities/check-post/api/changeEvidenceState"; + +export function useChangeEvidenceStatus(postId: number) { + const queryClient = useQueryClient(); + const router = useRouter(); + + const updatePostState = useCallback( + async (state: PostStatus) => { + try { + const res = await changeEvidenceState(postId, state); + + if (res.status === 204) { + toast.success("게시글 상태가 변경되었습니다."); + + await queryClient.invalidateQueries({ + queryKey: ["posts"], + exact: false, + }); + + router.back(); + } + } catch { + toast.error("게시글 상태 변경에 실패했습니다."); + } + }, + [postId, queryClient, router] + ); + + const handlePostState = useCallback( + (state: PostStatus) => (e: React.MouseEvent) => { + e.stopPropagation(); + void updatePostState(state); + }, + [updatePostState] + ); + + return { handlePostState }; +} diff --git a/apps/admin/src/views/check-post/model/useGetPosts.ts b/apps/admin/src/views/check-post/model/useGetPosts.ts index 58971106..f9bb8f04 100644 --- a/apps/admin/src/views/check-post/model/useGetPosts.ts +++ b/apps/admin/src/views/check-post/model/useGetPosts.ts @@ -1,13 +1,25 @@ -import type { postState } from "@repo/types/evidences"; +import type { PostStatus, PostType } from "@repo/types/evidences"; import { useQuery } from "@tanstack/react-query"; import { getPosts } from "../api/getPosts"; -export const useGetPosts = (email: string, status: postState | null) => { - return useQuery({ +export const useGetPosts = (email: string, status: PostStatus) => { + const query = useQuery({ queryKey: ["posts", email, status], queryFn: () => getPosts(email, status), staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 30, }); + + const posts: PostType[] = [ + ...(query.data?.data.majorActivityEvidence ?? []), + ...(query.data?.data.humanitiesActivityEvidence ?? []), + ...(query.data?.data.readingEvidence ?? []), + ...(query.data?.data.otherEvidence ?? []), + ]; + + return { + ...query, + posts, + }; }; diff --git a/apps/admin/src/views/detail/ui/index.tsx b/apps/admin/src/views/detail/ui/index.tsx index 6baeeaa4..1a376aa1 100644 --- a/apps/admin/src/views/detail/ui/index.tsx +++ b/apps/admin/src/views/detail/ui/index.tsx @@ -1,6 +1,6 @@ "use client"; -import type { post, postState } from "@repo/types/evidences"; +import type { PostStatus } from "@repo/types/evidences"; import { getCategoryName } from "@repo/utils/handleCategory"; import { isActivity, isOthers, isReading } from "@repo/utils/handlePost"; import Image from "next/image"; @@ -8,22 +8,25 @@ import { useParams, useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { toast } from "sonner"; -import { useGetStudent } from "@/entities/check-post/model/useGetStudent"; import { useMember } from "@/entities/member/model/memberContext"; -import { useChangeEvidenceState } from "@/views/check-post/model/useChangeEvidenceState"; +import { useGetMember } from "@/shared/model/useGetMember"; +import { useChangeEvidenceStatus } from "@/views/check-post/model/useChangeEvidenceStatus"; import { useGetPosts } from "@/views/check-post/model/useGetPosts"; export default function DetailView() { const { id } = useParams(); const { member: student, setMember } = useMember(); - const { handlePostState } = useChangeEvidenceState(Number(id)); + const { handlePostState } = useChangeEvidenceStatus(Number(id)); + const searchParams = useSearchParams(); const email = searchParams.get("email"); - const status = searchParams.get("status") as postState | null; - const { data: studentData, isError: isStudentError } = useGetStudent( + const status = searchParams.get("status") as PostStatus; + + const { data: studentData, isError: isStudentError } = useGetMember( decodeURIComponent(String(student?.email ?? email)), ); - const { data: studentPost, isError: isPostError } = useGetPosts( + + const { posts, isError: isPostError } = useGetPosts( String(student?.email ?? email), status, ); @@ -42,73 +45,66 @@ export default function DetailView() { toast.error("회원 정보를 불러오지 못했습니다."); } - const posts: post[] = [ - ...(studentPost?.data.majorActivityEvidence ?? []), - ...(studentPost?.data.humanitiesActivityEvidence ?? []), - ...(studentPost?.data.readingEvidence ?? []), - ...(studentPost?.data.otherEvidence ?? []), - ]; - const post = posts.find((post) => post.id === Number(id)); - let title = "Title"; - let subTitle = "Author"; - let content = ""; + if (!post) return

존재하지 않는 게시물입니다.

; - if (post) { - if (isActivity(post) || isReading(post)) { - title = post.title; - content = post.content; - } + const title = (() => { + if (isActivity(post) || isReading(post)) return post.title; + if (isOthers(post)) return post.evidenceType + return null; + })(); - if (isActivity(post) || isOthers(post)) { - subTitle = `카테고리: ${getCategoryName(post.categoryName)}`; - } else if (isReading(post) && post.author) { - subTitle = post.author; - } - } + const imageUri = isActivity(post) ? post.imageUri : null; + + const subTitle = (() => { + if (isReading(post)) return `${post.author}`; + if (isOthers(post)) return getCategoryName(post.categoryName); + if (isActivity(post)) return getCategoryName(post.categoryName); + return null; + })(); return (
-
-
-

+
+
+

{title}

-

- {post && isReading(post) ? post.author : "사용자"} {" . "} - {post && (isActivity(post) || isOthers(post)) - ? getCategoryName(post.categoryName) - : "Area"} -

-
- {post && - isActivity(post) && - post.imageUri != null && - post.imageUri !== "" ? ( -
+ {imageUri == null ? null : ( +
{post.title}
- ) : null} + )}

{subTitle}

-

- {content} +

+ {isOthers(post) ? ( + + 증빙 파일 보기 + + ) : ( + post.content + )}

-
- {post?.status === "PENDING" ? ( + {post.status === "PENDING" ? (

- - ) : null} -
- + + + ) : null} + ); }; diff --git a/apps/admin/src/views/score/api/featScore.ts b/apps/admin/src/views/score/api/featScore.ts deleted file mode 100644 index 69ddf906..00000000 --- a/apps/admin/src/views/score/api/featScore.ts +++ /dev/null @@ -1,33 +0,0 @@ -import instance from "@repo/api/axios"; -import { AxiosError, type AxiosResponse } from "axios"; - -interface FeatScoreResponse { - categoryName: string; - value: number; -} - -interface FeatScoreRequest { - email: string; - category: string; - score: number; -} - -export const featScore = async ( - email: string, - category: string, - score: number, -): Promise> => { - const data: FeatScoreRequest = { - email, - category, - score, - }; - try { - return await instance.patch("/score", data); - } catch (error) { - if (error instanceof AxiosError) { - throw error; - } - throw new Error("Unknown error occurred"); - } -}; diff --git a/apps/admin/src/views/score/api/patchScore.ts b/apps/admin/src/views/score/api/patchScore.ts new file mode 100644 index 00000000..6e371c04 --- /dev/null +++ b/apps/admin/src/views/score/api/patchScore.ts @@ -0,0 +1,30 @@ +import instance from "@repo/api/axios"; +import { isAxiosError, type AxiosResponse } from "axios"; + +import { extractStudentCode } from "@/shared/util/extractStudentId"; + +interface PatchScore { + categoryName: string; + value: number; +} + +export const patchScore = async ( + email: string, + category: string, + score: number +): Promise> => { + const data: PatchScore = { + categoryName: category, + value: score, + }; + const studentCode = extractStudentCode(email); + try { + const response = await instance.patch(`/score/${studentCode}`, data); + return response; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw error.response.data ?? "학생 점수를 수정하는 데 실패했습니다."; + } + throw error; + } +}; diff --git a/apps/admin/src/views/score/model/score.ts b/apps/admin/src/views/score/model/score.ts index c8970165..f0fdd0b5 100644 --- a/apps/admin/src/views/score/model/score.ts +++ b/apps/admin/src/views/score/model/score.ts @@ -4,8 +4,11 @@ export interface Score { } export interface ScoreFormType { - oneSemester: number | null; - twoSemester: number | null; - newrrow: number | null; - checkbox: boolean | undefined; + activity: number; + inAward: number; + outAward: number; + oneSemester: number; + twoSemester: number; + newrrow: number; + checkbox: boolean; } diff --git a/apps/admin/src/views/score/model/score_category.ts b/apps/admin/src/views/score/model/score_category.ts new file mode 100644 index 00000000..9d6b61fc --- /dev/null +++ b/apps/admin/src/views/score/model/score_category.ts @@ -0,0 +1,44 @@ +export const SCORE_CATEGORIES = { + AWARD_IN: { + value: "HUMANITIES-AWARD_CAREER-HUMANITY-IN_SCHOOL", + message: "교내인성영역관련수상", + field: "inAward", + isCheckbox: false, + }, + AWARD_OUT: { + field: "outAward", + value: "HUMANITIES-AWARD_CAREER-HUMANITY-OUT_SCHOOL", + message: "교외인성영역관련수상", + isCheckbox: false, + }, + ACTIVITY: { + value: "HUMANITIES-SERVICE-ACTIVITY", + message: "봉사활동", + field: "activity", + isCheckbox: false, + }, + SEMESTER_1: { + value: "HUMANITIES-SERVICE-CLUB_SEMESTER_1", + message: "1학기 봉사 시간", + field: "oneSemester", + isCheckbox: false, + }, + SEMESTER_2: { + value: "HUMANITIES-SERVICE-CLUB_SEMESTER_2", + message: "2학기 봉사 시간", + field: "twoSemester", + isCheckbox: false, + }, + NEWRROW: { + value: "HUMANITIES-ACTIVITIES-NEWRROW_S", + message: "뉴로우 참여 횟수", + field: "newrrow", + isCheckbox: false, + }, + TOEIC: { + value: "FOREIGN_LANG-ATTENDANCE-TOEIC_ACADEMY_STATUS", + message: "TOEIC 사관 학교 참여 여부", + field: "checkbox", + isCheckbox: true, + }, +} as const; diff --git a/apps/admin/src/views/score/ui/scoreForm.tsx b/apps/admin/src/views/score/ui/scoreForm.tsx index 375b6f2f..b371ab81 100644 --- a/apps/admin/src/views/score/ui/scoreForm.tsx +++ b/apps/admin/src/views/score/ui/scoreForm.tsx @@ -1,31 +1,27 @@ "use client"; +import type { HttpError } from "@/shared/model/error"; import { Button } from "@repo/shared/button"; import { Input } from "@repo/shared/input"; import { InputContainer } from "@repo/shared/inputContainer"; +import { useMutation } from "@tanstack/react-query"; +import { HttpStatusCode } from "axios"; import { useParams, useRouter } from "next/navigation"; import { useCallback } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { toast } from "sonner"; -import { featScore } from "../api/featScore"; +import { patchScore } from "../api/patchScore"; import type { ScoreFormType } from "../model/score"; +import { SCORE_CATEGORIES } from "../model/score_category"; import { Checkbox } from "@/entities/score/ui/checkbox"; -import Header from "@/shared/ui/header"; - -const SCORE_CATEGORIES = { - SEMESTER_1: "HUMANITIES-SERVICE-CLUB_SEMESTER_1", - SEMESTER_2: "HUMANITIES-SERVICE-CLUB_SEMESTER_2", - NEWRROW: "HUMANITIES-ACTIVITIES-NEWRROW_S", - TOEIC: "FOREIGN_LANG-ATTENDANCE-TOEIC_ACADEMY_STATUS", -} as const; const ScoreForm = () => { const { id } = useParams(); const router = useRouter(); - const { handleSubmit, control } = useForm({ + const { handleSubmit, control, formState: { isValid, errors } } = useForm({ mode: "onChange", }); @@ -33,102 +29,46 @@ const ScoreForm = () => { router.back(); }, [router]); - const renderCheckbox = useCallback( - ({ - field, - }: { - field: { value?: boolean; onChange: (value: boolean | null) => void }; - }) => , - [], - ); - - const { oneSemester, twoSemester, newrrow, checkbox } = useWatch({ control }); - - const isFormValid = Boolean( - (oneSemester !== undefined && oneSemester !== null && oneSemester > 0) || - (twoSemester !== undefined && twoSemester !== null && twoSemester > 0) || - (newrrow !== undefined && newrrow !== null && newrrow > 0) || - checkbox !== undefined, - ); - - const handleScoreSubmit = useCallback( - async (category: string, score: number, successMessage: string) => { - try { - const email = decodeURIComponent(String(id)); - const response = await featScore(email, category, score); - - if (response.status === 204) { - toast.success(successMessage); - return true; - } else { - toast.error(`${successMessage.replace(" 완료", "")} 실패`); - return false; - } - } catch { - toast.error("점수 추가 중 오류가 발생했습니다"); - return false; - } + const renderCheckbox = useCallback(({ field }: { + field: { value?: boolean; onChange: (value: boolean | null) => void }; + }) => , []); + + const { mutate, isPending } = useMutation({ + mutationFn: async (data: ScoreFormType) => { + return Promise.all( + Object.values(SCORE_CATEGORIES).map(async (category) => { + const score = data[category.field as keyof ScoreFormType]; + return patchScore( + decodeURIComponent(String(id)), + category.value, + typeof score === "boolean" ? (score ? 0 : 1) : score + ); + }) + ); }, - [id], - ); - - const onSubmit = useCallback( - async (data: ScoreFormType) => { - let success = true; - - if (data.oneSemester !== null && data.oneSemester > 0) { - success = - (await handleScoreSubmit( - SCORE_CATEGORIES.SEMESTER_1, - data.oneSemester, - "1학기 봉사 시간 점수 추가 완료", - )) && success; - } - - if (data.twoSemester !== null && data.twoSemester > 0) { - success = - (await handleScoreSubmit( - SCORE_CATEGORIES.SEMESTER_2, - data.twoSemester, - "2학기 봉사 시간 점수 추가 완료", - )) && success; - } - - if (data.newrrow !== null && data.newrrow > 0) { - success = - (await handleScoreSubmit( - SCORE_CATEGORIES.NEWRROW, - data.newrrow, - "뉴로우 참여 횟수 점수 추가 완료", - )) && success; - } - - if (data.checkbox !== undefined) { - success = - (await handleScoreSubmit( - SCORE_CATEGORIES.TOEIC, - data.checkbox ? 1 : 0, - "TOEIC 참여 여부 점수 추가 완료", - )) && success; + onSuccess: () => { + toast.success("모든 점수 부여 성공"); + router.push("/"); + }, + onError: (error: HttpError) => { + if (error.httpStatus == HttpStatusCode.NotFound) { + toast.error("해당하는 카테고리가 존재하지 않습니다."); + } else { + toast.error("점수 부여 중 오류가 발생 했습니다.") } + } + }); - if (success) { - router.push("/"); - } - }, - [handleScoreSubmit, router], - ); + const onSubmit = useCallback((data: ScoreFormType) => { + mutate(data); + }, [mutate]); - const handleFormSubmit = useCallback( - (e) => { - void handleSubmit(onSubmit)(e); - }, - [handleSubmit, onSubmit], - ); + const handleFormSubmit = useCallback((e) => { + void handleSubmit(onSubmit)(e); + }, [handleSubmit, onSubmit]); return (
-
{

점수 추가

- - - - + - +
+ + + + + + +
+
+ + + + + + +
+ @@ -174,7 +157,7 @@ const ScoreForm = () => { +
+ + - - - value === formValues.password || "비밀번호가 일치하지 않습니다.", - }} - type="password" - /> + +
+ passwordCheck === password || "비밀번호가 일치하지 않습니다.", + }} + type={showPasswordCheck ? "text" : "password"} + /> + +
+ +