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 : (
- ) : 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 : (
+
- ) : null}
+ )}
{subTitle}
-
- {content}
+
+ {isOthers(post) ? (
+
+ 증빙 파일 보기
+
+ ) : (
+ post.content
+ )}
-
- {post?.status === "PENDING" ? (
+ {post.status === "PENDING" ? (
{
const [click, setClick] = useState(null);
@@ -25,11 +24,11 @@ const MemberView = () => {
const [classNumber, setClassNumber] = useState();
const [name, setName] = useState();
- const { data, error, isLoading } = useGetMember();
+ const { data, isError } = useGetMembers();
- if (error) toast.error("학생 목록을 불러오지 못했습니다.");
+ if (isError) toast.error("학생 목록을 불러오지 못했습니다.");
- const members = (data?.data ?? []) as Member[];
+ const members = (data?.data ?? []);
const resetFilter = useCallback((): void => {
setGrade(undefined);
@@ -40,6 +39,7 @@ const MemberView = () => {
const handleOpen = useCallback((): void => {
setOpen((prev) => !prev);
}, []);
+
const handleCardClick = useCallback(
async (email: string): Promise => {
setClick(email);
@@ -83,88 +83,78 @@ const MemberView = () => {
}, [handleSearch]);
return (
-
-
-
-
-
-
- {(() => {
- if (isLoading) {
- loading...
;
+
+
+
+ {(() => {
+ const target = result.length > 0 ? result : members;
+ return target.map((member) => (
+ 0 ? result : members;
- return target.map((member: Member) => (
-
- ));
- })()}
-
-
-
- {student === undefined ? (
-
-
-
- 학생을 선택해주세요
-
-
- ) : (
-
- )}
-
- {open ? (
- <>
-
-
-
+ {student === undefined ? (
+
+
+
+ 학생을 선택해주세요
+
+
+ ) : (
+
+ )}
+ {open ? (
+ <>
+
+
-
+
+ >
+ ) : 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 (
diff --git a/apps/client/src/entities/posts/api/getSearchResult.ts b/apps/client/src/entities/posts/api/getSearchResult.ts
index e9f7d7a2..f5f45128 100644
--- a/apps/client/src/entities/posts/api/getSearchResult.ts
+++ b/apps/client/src/entities/posts/api/getSearchResult.ts
@@ -1,14 +1,20 @@
import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
+import type { PostResponse } from "@repo/types/evidences";
+import { isAxiosError } from "axios";
export const getSearchResult = async (
query: string,
- type?: string,
-): Promise => {
+ type?: string
+): Promise => {
try {
- const res = instance.get(`/evidence/search?title=${query}&type=${type}`);
- return res;
+ const response = await instance.get(
+ `/evidence/search?title=${query}&type=${type}`
+ );
+ return response.data;
} catch (error) {
- return error as AxiosError;
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "증빙 자료 검색 실패";
+ }
+ throw error;
}
};
diff --git a/apps/client/src/entities/posts/lib/useGetDraft.ts b/apps/client/src/entities/posts/lib/useGetDraft.ts
deleted file mode 100644
index ae1b3760..00000000
--- a/apps/client/src/entities/posts/lib/useGetDraft.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { DraftResponse } from "@repo/types/draft";
-import { useQuery } from "@tanstack/react-query";
-
-import { getDraft } from "@/views/posts/api/getDraft";
-
-export const useGetDraft = () => {
- return useQuery({
- queryKey: ["drafts"],
- queryFn: getDraft,
- });
-};
diff --git a/apps/client/src/entities/posts/lib/useGetPosts.ts b/apps/client/src/entities/posts/lib/useGetPosts.ts
deleted file mode 100644
index cbaaa4f1..00000000
--- a/apps/client/src/entities/posts/lib/useGetPosts.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { EvidenceType } from "@repo/types/evidences";
-import { useQuery } from "@tanstack/react-query";
-
-import { getPosts } from "@/views/posts/api/getPosts";
-
-export const useGetPosts = (type: EvidenceType | "DRAFT" | null) => {
- return useQuery({
- queryKey: ["posts", type],
- queryFn: () => getPosts(type as EvidenceType | null),
- enabled: type !== "DRAFT",
- });
-};
diff --git a/apps/client/src/entities/posts/ui/search.tsx b/apps/client/src/entities/posts/ui/search.tsx
index d211e7df..5c647e2a 100644
--- a/apps/client/src/entities/posts/ui/search.tsx
+++ b/apps/client/src/entities/posts/ui/search.tsx
@@ -2,14 +2,14 @@
import { useDebounce } from "@repo/hooks/useDebounce";
import SearchIcon from "@repo/shared/search";
-import type { EvidenceResponse } from "@repo/types/evidences";
+import type { PostResponse } from "@repo/types/evidences";
import { useCallback, useEffect } from "react";
import { toast } from "sonner";
import { getSearchResult } from "../api/getSearchResult";
interface SearchProps {
- setResult: (result: EvidenceResponse) => void;
+ setResult: (result: PostResponse) => void;
search: string;
setSearch: (search: string) => void;
type?: string;
@@ -21,16 +21,14 @@ const Search = ({ setResult, search, type, setSearch }: SearchProps) => {
useEffect(() => {
const fetchSearchResult = async () => {
if (!debouncedValue) return;
-
try {
const search = await getSearchResult(debouncedValue, type);
- setResult(search.data as EvidenceResponse);
+ setResult(search);
} catch {
- toast.error("게시물 검색중 오류가 발생했습니다.");
+ toast.error(type == "DRAFT" ? "임시저장 게시물은 검색하실 수 없습니다." : "게시물 검색중 오류가 발생했습니다.")
}
};
- // Promise를 명시적으로 처리
void fetchSearchResult();
}, [debouncedValue, setResult, type]);
diff --git a/apps/client/src/entities/signin/api/postSignin.ts b/apps/client/src/entities/signin/api/postSignin.ts
index cba009bb..29cd4ef0 100644
--- a/apps/client/src/entities/signin/api/postSignin.ts
+++ b/apps/client/src/entities/signin/api/postSignin.ts
@@ -1,15 +1,16 @@
-import instance from "@repo/api/axios";
-import { isAxiosError } from "axios";
-
import type {
SigninFormProps,
SigninFormResponse,
-} from "@shared/model/AuthForm";
+} from "@/shared/model/signin";
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
-export const postSignin = async (form: SigninFormProps) => {
+export const postSignin = async (
+ form: SigninFormProps
+): Promise> => {
try {
const response = await instance.post(`/auth/signin`, form);
- return response.data as SigninFormResponse;
+ return response;
} catch (error) {
if (isAxiosError(error) && error.response) {
throw error.response.data ?? "로그인 실패";
diff --git a/apps/client/src/entities/signup/api/patchVerifyEmail.ts b/apps/client/src/entities/signup/api/patchVerifyEmail.ts
index 3799ee11..ca9f6144 100644
--- a/apps/client/src/entities/signup/api/patchVerifyEmail.ts
+++ b/apps/client/src/entities/signup/api/patchVerifyEmail.ts
@@ -1,7 +1,9 @@
import instance from "@repo/api/axios";
-import { isAxiosError } from "axios";
+import { isAxiosError, type AxiosResponse } from "axios";
-export const patchVerifyEmail = async (code: number) => {
+export const patchVerifyEmail = async (
+ code: number
+): Promise => {
try {
const response = await instance.patch(`/auth/verify-email`, { code });
return response;
diff --git a/apps/client/src/entities/signup/api/postSendEmail.ts b/apps/client/src/entities/signup/api/postSendEmail.ts
index 3228a169..1da78afb 100644
--- a/apps/client/src/entities/signup/api/postSendEmail.ts
+++ b/apps/client/src/entities/signup/api/postSendEmail.ts
@@ -1,7 +1,7 @@
import instance from "@repo/api/axios";
-import { isAxiosError } from "axios";
+import { isAxiosError, type AxiosResponse } from "axios";
-export const postSendEmail = async (email: string) => {
+export const postSendEmail = async (email: string): Promise => {
try {
const response = await instance.post(`/auth/send-email`, { email });
return response;
diff --git a/apps/client/src/entities/signup/api/postSignup.ts b/apps/client/src/entities/signup/api/postSignup.ts
index 844c29b1..deabdbb7 100644
--- a/apps/client/src/entities/signup/api/postSignup.ts
+++ b/apps/client/src/entities/signup/api/postSignup.ts
@@ -1,10 +1,9 @@
+import type { SignupFormProps } from "@/shared/model/signup";
import instance from "@repo/api/axios";
import { isAxiosError, type AxiosResponse } from "axios";
-import type { SignupFormProps } from "@shared/model/AuthForm";
-
export const postSignup = async (
- form: SignupFormProps,
+ form: SignupFormProps
): Promise => {
try {
const response = await instance.post(`/auth/signup`, form);
diff --git a/apps/client/src/shared/api/getDraft.ts b/apps/client/src/shared/api/getDraft.ts
new file mode 100644
index 00000000..b6bf6a65
--- /dev/null
+++ b/apps/client/src/shared/api/getDraft.ts
@@ -0,0 +1,17 @@
+import instance from "@repo/api/axios";
+import type { DraftResponse } from "@repo/types/draft";
+import { isAxiosError } from "axios";
+
+export const getDraft = async () => {
+ try {
+ const response = await instance.get(
+ "/evidence/current/draft"
+ );
+ return response;
+ } catch (error) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "임시저장 게시물 조회 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/shared/api/getPosts.ts b/apps/client/src/shared/api/getPosts.ts
new file mode 100644
index 00000000..7564f3be
--- /dev/null
+++ b/apps/client/src/shared/api/getPosts.ts
@@ -0,0 +1,6 @@
+import instance from "@repo/api/axios";
+import type { PostResponse, EvidenceType } from "@repo/types/evidences";
+
+export const getPosts = async (type: EvidenceType) => {
+ return await instance.get(`/evidence/current?type=${type}`);
+};
diff --git a/apps/client/src/shared/api/fixScore.ts b/apps/client/src/shared/api/postScore.ts
similarity index 85%
rename from apps/client/src/shared/api/fixScore.ts
rename to apps/client/src/shared/api/postScore.ts
index 6557cadc..e4e767d5 100644
--- a/apps/client/src/shared/api/fixScore.ts
+++ b/apps/client/src/shared/api/postScore.ts
@@ -1,13 +1,13 @@
import instance from "@repo/api/axios";
import { isAxiosError, type AxiosResponse } from "axios";
-interface FixScore {
+interface PostScore {
categoryName: string;
file: File;
value: number;
}
-export const FixScore = async (data: FixScore): Promise => {
+export const PostScore = async (data: PostScore): Promise => {
try {
const formData = new FormData();
formData.append("file", data.file);
diff --git a/apps/client/src/shared/api/sendScore.ts b/apps/client/src/shared/api/sendScore.ts
deleted file mode 100644
index 5ba77ff1..00000000
--- a/apps/client/src/shared/api/sendScore.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
-
-export const sendScore = async (
- formData: FormData,
-): Promise => {
- try {
- const res = await instance.post("/evidence/current/scoring", formData);
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
diff --git a/apps/client/src/shared/api/updateActivity.ts b/apps/client/src/shared/api/updateActivity.ts
deleted file mode 100644
index 4061a49e..00000000
--- a/apps/client/src/shared/api/updateActivity.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
-
-export const updateMajorActivity = async (
- evidenceId: number,
- activity: FormData,
-): Promise => {
- try {
- const res = await instance.patch(`evidence/major/${evidenceId}`, activity);
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
-
-export const updateHumanitiesActivity = async (
- evidenceId: number,
- activity: FormData,
-): Promise => {
- try {
- const res = await instance.patch(
- `evidence/humanities/${evidenceId}`,
- activity,
- );
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
diff --git a/apps/client/src/shared/api/updateOthers.ts b/apps/client/src/shared/api/updateOthers.ts
deleted file mode 100644
index 39a944fc..00000000
--- a/apps/client/src/shared/api/updateOthers.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
-
-export const updateOthers = async (
- evidenceId: number,
- formData: FormData,
-): Promise => {
- try {
- const res = await instance.patch(`evidence/other/${evidenceId}`, formData);
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
diff --git a/apps/client/src/shared/api/updateReading.ts b/apps/client/src/shared/api/updateReading.ts
deleted file mode 100644
index ab775466..00000000
--- a/apps/client/src/shared/api/updateReading.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import instance from "@repo/api/axios";
-
-import type { Book } from "@/widgets/write/model/book";
-
-export const updateReading = async (evidenceId: number, bookData: Book) => {
- await instance.patch(`evidence/reading/${evidenceId}`, bookData);
-};
diff --git a/apps/client/src/shared/model/useGetCurrentMember.ts b/apps/client/src/shared/lib/useGetCurrentMember.ts
similarity index 100%
rename from apps/client/src/shared/model/useGetCurrentMember.ts
rename to apps/client/src/shared/lib/useGetCurrentMember.ts
diff --git a/apps/client/src/shared/lib/useGetDraft.ts b/apps/client/src/shared/lib/useGetDraft.ts
new file mode 100644
index 00000000..4b950d46
--- /dev/null
+++ b/apps/client/src/shared/lib/useGetDraft.ts
@@ -0,0 +1,23 @@
+import type { DraftType } from "@repo/types/draft";
+import type { EvidenceType } from "@repo/types/evidences";
+import { useQuery } from "@tanstack/react-query";
+
+import { getDraft } from "../api/getDraft";
+
+export const useGetDraft = (type: EvidenceType | "DRAFT" | null) => {
+ const query = useQuery({
+ queryKey: ["drafts", type],
+ queryFn: getDraft,
+ enabled: type == "DRAFT",
+ });
+
+ const drafts: DraftType[] = [
+ ...(query.data?.data.activityEvidences ?? []),
+ ...(query.data?.data.readingEvidences ?? []),
+ ];
+
+ return {
+ ...query,
+ drafts,
+ };
+};
diff --git a/apps/client/src/shared/lib/useGetPosts.ts b/apps/client/src/shared/lib/useGetPosts.ts
new file mode 100644
index 00000000..1ecf13a2
--- /dev/null
+++ b/apps/client/src/shared/lib/useGetPosts.ts
@@ -0,0 +1,24 @@
+import type { EvidenceType, PostType } from "@repo/types/evidences";
+import { useQuery } from "@tanstack/react-query";
+
+import { getPosts } from "../api/getPosts";
+
+export const useGetPosts = (type: EvidenceType | "DRAFT") => {
+ const query = useQuery({
+ queryKey: ["posts", type],
+ queryFn: () => getPosts(type as EvidenceType),
+ enabled: type !== "DRAFT",
+ });
+
+ 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/client/src/shared/mocks/data/evidenceMock.json b/apps/client/src/shared/mocks/data/evidenceMock.json
index a45b55b9..f48b0b45 100644
--- a/apps/client/src/shared/mocks/data/evidenceMock.json
+++ b/apps/client/src/shared/mocks/data/evidenceMock.json
@@ -1,119 +1,132 @@
-[
- {
- "id": 1,
- "title": "AWS를 이용하는 방법",
- "content": "2024년 9월 12일에 진행된 4시간짜리 AWS 교육에 참여하여 AWS VPC, AWS Lambda, AWS S3, AWS EC2 등 현대 소프트웨어 개발에서 핵심이 되는 서비스들을 학습하고 실습하였다. 학생으로서는 알기 힘든 AWS의 요금제, VPC 구성 방법 등을 실습을 통해 익히고, AI 서비스인 PartyRock을 활용한 애플리케이션도 제작해보며 AWS 생태계 전반을 이해하는 데 큰 도움이 되었다. 교육 후 임시 계정을 발급받아 자율 실습도 진행하며 다양한 시도를 해볼 수 있었다.",
- "categoryName": "전공영역-전공특강(방과후) 참가",
- "status": "APPROVE",
- "imageUri": "/mocks/aws.png"
- },
- {
- "id": 2,
- "title": "한전 KDN 소프트웨어 경진대회",
- "content": "2024년 11월 27일, 나주 한전 KDN 본사에서 개최된 소프트웨어 경진대회에 참가하여 Bstorm이라는 치매 예방 목적의 소프트웨어를 개발 및 발표하였다. 나는 서버 개발을 담당하였으며, 약 40개의 API를 설계 및 구현하고 CI 파이프라인까지 구축하였다. 모놀리식 아키텍처에 대한 이해를 넓힐 수 있었고, Git Flow를 기반으로 한 협업 방식도 경험하였다. 끝까지 버그를 해결하며 노력한 결과, 장려상을 수상하였다.",
- "categoryName": "전공영역-공문을 통한 전공분야 대회 참여",
- "status": "APPROVE",
- "imageUri": "/mocks/KDN.png"
- },
- {
- "id": 3,
- "title": "모두의 HTML5 & CSS3",
- "author": "김인권",
- "page": 324,
- "content": "HTML과 CSS의 기초를 쉽고 재미있게 배울 수 있도록 구성된 입문서이다. 실제 예제를 기반으로 HTML5 태그와 CSS3의 스타일링 기법을 설명하고, 반응형 웹 디자인까지 다루며 웹 개발 초보자에게 실용적인 내용을 제공한다.",
- "status": "APPROVE"
- },
- {
- "id": 4,
- "fileUrl": "/mocks/toefl.png",
- "evidenceType": "TOEFL",
- "categoryName": "외국어영역-TOEFL 점수",
- "status": "APPROVE"
- },
- {
- "id": 5,
- "title": "제 27회 앱잼 참여",
- "content": "Alpaco에서 주최한 제27회 앱잼에 참여하여 '가치 하다'라는 앱을 개발하였다. 백엔드 개발을 맡아 Spring Boot와 Kotlin을 사용하였고, Git을 통한 협업의 어려움을 직접 겪으며 해결책을 찾아갔다. 앱 개발이라는 환경에 적응하며 REST API 기반으로 개발을 진행했고, 협업과 시간 관리의 중요성을 체감할 수 있었다. 생활정보 분야에서 최우수상을 수상하였다.",
- "categoryName": "전공영역-전공 분야 대회 개별 참여",
- "status": "APPROVE",
- "imageUri": "/mocks/appjam.png"
- },
- {
- "id": 6,
- "title": "C언어 코딩 콘테스트 참여",
- "content": "2024년 5월부터 6월까지 진행된 교내 C언어 코딩 콘테스트에서 DB 서버와 curl 라이브러리를 활용해 실시간 온라인 게임인 'project return(회귀)'를 개발하였다. 포인터 누수로 인한 DB 연결 중단 문제를 해결하기 위해 포인터 관리 기능을 도입하였고, 이메일 전송 기능도 직접 구현하였다. 총 누적 75,000줄에 달하는 코드 작업 끝에 최우수상을 수상하였다.",
- "categoryName": "전공영역-교내 해커톤 대회",
- "status": "APPROVE"
- },
- {
- "id": 7,
- "fileUrl": "/mocks/ReadingMarathon.png",
- "evidenceType": "READ-A-THON",
- "categoryName": "인성영역-독서마라톤 토끼코스 이상",
- "status": "APPROVE"
- },
- {
- "id": 8,
- "title": "전기CAD기능사",
- "content": "2025년 1월부터 시작하여 3월까지 방과후 및 정규 수업을 통해 전기CAD기능사 자격증 필기를 준비하고, 이후 실기시험 대비까지 병행하였다. 학교 실습실을 활용해 꾸준히 실습하였고 5월 초에 자격증을 최종 취득하였다.",
- "categoryName": "인성영역-자기주도적 활동",
- "status": "APPROVE"
- },
- {
- "id": 9,
- "fileUrl": "/mocks/toeic.png",
- "evidenceType": "TOEIC",
- "categoryName": "외국어영역-TOEIC 점수",
- "status": "APPROVE"
- },
- {
- "id": 10,
- "title": "카이스트 진로 강의",
- "content": "디지털 P:UM 프로젝트의 일환으로 KAIST 견학 프로그램에 참여하여 AI 및 소프트웨어 분야의 진로 강의를 들었다. AI 시대에서 개발자가 갖춰야 할 자세와 중장기 계획 수립에 대해 실제 사례와 함께 구체적으로 배울 수 있었다.",
- "categoryName": "전공영역-전공 관련 교육프로그램 참여",
- "status": "APPROVE",
- "imageUri": "/mocks/pum.png"
- },
- {
- "id": 11,
- "fileUrl": "/mocks/certificate.png",
- "evidenceType": "CERTIFICATE",
- "categoryName": "인성영역-한자자격증",
- "status": "APPROVE"
- },
- {
- "id": 12,
- "title": "한국사 자격증 1급",
- "content": "한국사 능력 검정 시험 1급 취득.",
- "categoryName": "인성영역-한국사자격증",
- "status": "APPROVE"
- },
- {
- "id": 13,
- "title": "아이디어 페스티벌",
- "content": "학년별로 팀을 구성해 1년 동안 전공 수업을 통해 준비한 프로젝트를 발표하는 교내 행사에 참여하였다. 팀워크와 실무 경험을 통해 협업의 중요성과 발표 능력을 키울 수 있었다.",
- "categoryName": "전공영역-GSM FESTIVAL",
- "status": "APPROVE"
- },
- {
- "id": 14,
- "title": "전공 캠프",
- "content": "방학 동안 1주일간 학교에서 진행된 전공 캠프에 참여하여 Python, 백엔드, Git, 그리고 TOPCIT 대비 교육을 받았다. 실습 위주의 수업을 통해 백엔드 개발과 형상관리 도구에 대한 이해를 높일 수 있었고, 진로에 대한 방향을 설정하는 데 큰 도움이 되었다.",
- "categoryName": "전공영역-전공관련 방과후학교 이수",
- "status": "APPROVE"
- },
- {
- "id": 15,
- "fileUrl": "/mocks/toeiccertificate.png",
- "evidenceType": "TOEIC",
- "categoryName": "외국어영역-토사관 참여",
- "status": "APPROVE"
- },
- {
- "id": 16,
- "fileUrl": "/mocks/multireadingaward.png",
- "evidenceType": "CERTIFICATE",
- "categoryName": "인성영역-교내인성영역관련수상",
- "status": "APPROVE"
- }
-]
+{
+ "majorActivityEvidence": [
+ {
+ "id": 1,
+ "title": "AWS를 이용하는 방법",
+ "content": "2024년 9월 12일에 진행된 4시간짜리 AWS 교육에 참여하여 AWS VPC, AWS Lambda, AWS S3, AWS EC2 등 현대 소프트웨어 개발에서 핵심이 되는 서비스들을 학습하고 실습하였다. 학생으로서는 알기 힘든 AWS의 요금제, VPC 구성 방법 등을 실습을 통해 익히고, AI 서비스인 PartyRock을 활용한 애플리케이션도 제작해보며 AWS 생태계 전반을 이해하는 데 큰 도움이 되었다. 교육 후 임시 계정을 발급받아 자율 실습도 진행하며 다양한 시도를 해볼 수 있었다.",
+ "categoryName": "전공영역-전공특강(방과후) 참가",
+ "status": "APPROVE",
+ "imageUri": "/mocks/aws.png"
+ },
+ {
+ "id": 2,
+ "title": "한전 KDN 소프트웨어 경진대회",
+ "content": "2024년 11월 27일, 나주 한전 KDN 본사에서 개최된 소프트웨어 경진대회에 참가하여 Bstorm이라는 치매 예방 목적의 소프트웨어를 개발 및 발표하였다. 나는 서버 개발을 담당하였으며, 약 40개의 API를 설계 및 구현하고 CI 파이프라인까지 구축하였다. 모놀리식 아키텍처에 대한 이해를 넓힐 수 있었고, Git Flow를 기반으로 한 협업 방식도 경험하였다. 끝까지 버그를 해결하며 노력한 결과, 장려상을 수상하였다.",
+ "categoryName": "전공영역-공문을 통한 전공분야 대회 참여",
+ "status": "APPROVE",
+ "imageUri": "/mocks/KDN.png"
+ },
+ {
+ "id": 5,
+ "title": "제 27회 앱잼 참여",
+ "content": "Alpaco에서 주최한 제27회 앱잼에 참여하여 '가치 하다'라는 앱을 개발하였다. 백엔드 개발을 맡아 Spring Boot와 Kotlin을 사용하였고, Git을 통한 협업의 어려움을 직접 겪으며 해결책을 찾아갔다. 앱 개발이라는 환경에 적응하며 REST API 기반으로 개발을 진행했고, 협업과 시간 관리의 중요성을 체감할 수 있었다. 생활정보 분야에서 최우수상을 수상하였다.",
+ "categoryName": "전공영역-전공 분야 대회 개별 참여",
+ "status": "APPROVE",
+ "imageUri": "/mocks/appjam.png"
+ },
+ {
+ "id": 6,
+ "title": "C언어 코딩 콘테스트 참여",
+ "content": "2024년 5월부터 6월까지 진행된 교내 C언어 코딩 콘테스트에서 DB 서버와 curl 라이브러리를 활용해 실시간 온라인 게임인 'project return(회귀)'를 개발하였다. 포인터 누수로 인한 DB 연결 중단 문제를 해결하기 위해 포인터 관리 기능을 도입하였고, 이메일 전송 기능도 직접 구현하였다. 총 누적 75,000줄에 달하는 코드 작업 끝에 최우수상을 수상하였다.",
+ "categoryName": "전공영역-교내 해커톤 대회",
+ "status": "APPROVE",
+ "imageUri": null
+ },
+ {
+ "id": 10,
+ "title": "카이스트 진로 강의",
+ "content": "디지털 P:UM 프로젝트의 일환으로 KAIST 견학 프로그램에 참여하여 AI 및 소프트웨어 분야의 진로 강의를 들었다. AI 시대에서 개발자가 갖춰야 할 자세와 중장기 계획 수립에 대해 실제 사례와 함께 구체적으로 배울 수 있었다.",
+ "categoryName": "전공영역-전공 관련 교육프로그램 참여",
+ "status": "APPROVE",
+ "imageUri": "/mocks/pum.png"
+ },
+ {
+ "id": 13,
+ "title": "아이디어 페스티벌",
+ "content": "학년별로 팀을 구성해 1년 동안 전공 수업을 통해 준비한 프로젝트를 발표하는 교내 행사에 참여하였다. 팀워크와 실무 경험을 통해 협업의 중요성과 발표 능력을 키울 수 있었다.",
+ "categoryName": "전공영역-GSM FESTIVAL",
+ "status": "APPROVE",
+ "imageUri": null
+ },
+ {
+ "id": 14,
+ "title": "전공 캠프",
+ "content": "방학 동안 1주일간 학교에서 진행된 전공 캠프에 참여하여 Python, 백엔드, Git, 그리고 TOPCIT 대비 교육을 받았다. 실습 위주의 수업을 통해 백엔드 개발과 형상관리 도구에 대한 이해를 높일 수 있었고, 진로에 대한 방향을 설정하는 데 큰 도움이 되었다.",
+ "categoryName": "전공영역-전공관련 방과후학교 이수",
+ "status": "APPROVE",
+ "imageUri": null
+ }
+ ],
+ "humanitiesActivityEvidence": [
+ {
+ "id": 8,
+ "title": "전기CAD기능사",
+ "content": "2025년 1월부터 시작하여 3월까지 방과후 및 정규 수업을 통해 전기CAD기능사 자격증 필기를 준비하고, 이후 실기시험 대비까지 병행하였다. 학교 실습실을 활용해 꾸준히 실습하였고 5월 초에 자격증을 최종 취득하였다.",
+ "categoryName": "인성영역-자기주도적 활동",
+ "status": "APPROVE",
+ "imageUri": null
+ },
+ {
+ "id": 12,
+ "title": "한국사 자격증 1급",
+ "content": "한국사 능력 검정 시험 1급 취득.",
+ "categoryName": "인성영역-한국사자격증",
+ "status": "APPROVE",
+ "imageUri": null
+ }
+ ],
+ "readingEvidence": [
+ {
+ "id": 3,
+ "title": "모두의 HTML5 & CSS3",
+ "author": "김인권",
+ "page": 324,
+ "content": "HTML과 CSS의 기초를 쉽고 재미있게 배울 수 있도록 구성된 입문서이다. 실제 예제를 기반으로 HTML5 태그와 CSS3의 스타일링 기법을 설명하고, 반응형 웹 디자인까지 다루며 웹 개발 초보자에게 실용적인 내용을 제공한다.",
+ "status": "APPROVE"
+ }
+ ],
+ "otherEvidence": [
+ {
+ "id": 4,
+ "fileUri": "/mocks/toefl.png",
+ "evidenceType": "TOEFL",
+ "categoryName": "외국어영역-TOEFL 점수",
+ "status": "APPROVE"
+ },
+ {
+ "id": 7,
+ "fileUri": "/mocks/ReadingMarathon.png",
+ "evidenceType": "READ-A-THON",
+ "categoryName": "인성영역-독서마라톤 토끼코스 이상",
+ "status": "APPROVE"
+ },
+ {
+ "id": 9,
+ "fileUri": "/mocks/toeic.png",
+ "evidenceType": "TOEIC",
+ "categoryName": "외국어영역-TOEIC 점수",
+ "status": "APPROVE"
+ },
+ {
+ "id": 11,
+ "fileUri": "/mocks/certificate.png",
+ "evidenceType": "CERTIFICATE",
+ "categoryName": "인성영역-한자자격증",
+ "status": "APPROVE"
+ },
+ {
+ "id": 15,
+ "fileUri": "/mocks/toeiccertificate.png",
+ "evidenceType": "TOEIC",
+ "categoryName": "외국어영역-토사관 참여",
+ "status": "APPROVE"
+ },
+ {
+ "id": 16,
+ "fileUri": "/mocks/multireadingaward.png",
+ "evidenceType": "CERTIFICATE",
+ "categoryName": "인성영역-교내인성영역관련수상",
+ "status": "APPROVE"
+ }
+ ]
+}
diff --git a/apps/client/src/shared/model/AuthForm.ts b/apps/client/src/shared/model/AuthForm.ts
deleted file mode 100644
index c3b0f5f5..00000000
--- a/apps/client/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/client/src/shared/model/certification.ts b/apps/client/src/shared/model/certification.ts
new file mode 100644
index 00000000..e107d5af
--- /dev/null
+++ b/apps/client/src/shared/model/certification.ts
@@ -0,0 +1,12 @@
+export interface CertificationRequest {
+ name: string;
+ file: File;
+ acquisitionDate: string;
+}
+
+export interface CertificationResponse {
+ id: number;
+ name: string;
+ acquisitionDate: Date;
+ evidenceUri: string;
+}
diff --git a/apps/client/src/shared/model/changePWForm.ts b/apps/client/src/shared/model/changePWForm.ts
deleted file mode 100644
index 7e6c4455..00000000
--- a/apps/client/src/shared/model/changePWForm.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { AuthStepForm } from "./AuthForm";
-
-export interface ChangePasswordProps {
- email: string;
- password: string;
-}
-
-export type ChangePW_AuthStepForm = Omit;
-
-export interface ChangePasswordStepForm {
- password: string;
- passwordCheck: string;
-}
diff --git a/apps/client/src/shared/model/changePassword.ts b/apps/client/src/shared/model/changePassword.ts
new file mode 100644
index 00000000..6c76533c
--- /dev/null
+++ b/apps/client/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/client/src/shared/model/config.ts b/apps/client/src/shared/model/config.ts
new file mode 100644
index 00000000..bdcf8fb2
--- /dev/null
+++ b/apps/client/src/shared/model/config.ts
@@ -0,0 +1 @@
+export type ConfigType = "major" | "humanities" | "reading" | "others";
diff --git a/apps/client/src/shared/types/error.ts b/apps/client/src/shared/model/error.ts
similarity index 100%
rename from apps/client/src/shared/types/error.ts
rename to apps/client/src/shared/model/error.ts
diff --git a/apps/client/src/shared/model/formValues.ts b/apps/client/src/shared/model/formValues.ts
new file mode 100644
index 00000000..77fb047f
--- /dev/null
+++ b/apps/client/src/shared/model/formValues.ts
@@ -0,0 +1,15 @@
+export interface Option {
+ name: string;
+ send: string;
+}
+
+export interface FormValues {
+ title: string;
+ content: string;
+ categoryName?: Option;
+ file?: File;
+ author?: string;
+ page?: number;
+ value?: number;
+ draftId?: string;
+}
diff --git a/apps/client/src/shared/model/signin.ts b/apps/client/src/shared/model/signin.ts
new file mode 100644
index 00000000..cb9c8410
--- /dev/null
+++ b/apps/client/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/client/src/shared/model/signup.ts b/apps/client/src/shared/model/signup.ts
new file mode 100644
index 00000000..90ea1090
--- /dev/null
+++ b/apps/client/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/client/src/shared/ui/index.ts b/apps/client/src/shared/ui/index.ts
index fa39e928..f68be711 100644
--- a/apps/client/src/shared/ui/index.ts
+++ b/apps/client/src/shared/ui/index.ts
@@ -1,4 +1,4 @@
export { default as Dropdown } from "./dropdown";
export { default as File } from "./file";
export { default as Textarea } from "./textarea";
-export { default as Post } from "./post/post";
+export { default as Post } from "./post";
diff --git a/apps/client/src/shared/ui/post/index.tsx b/apps/client/src/shared/ui/post/index.tsx
new file mode 100644
index 00000000..7556abdf
--- /dev/null
+++ b/apps/client/src/shared/ui/post/index.tsx
@@ -0,0 +1,66 @@
+import type { DraftType } from "@repo/types/draft";
+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: PostType | DraftType;
+ isExample?: boolean;
+ onClick?: () => void;
+}
+
+const Post = ({ data, isExample = false, 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 (
+
+
+ {imageUri == null ? null : (
+
+ )}
+
+
+
+
+ {title}
+
+
+ {subTitle}
+
+ {!isExample && state ?
+
+ {handleState(state)}
+ : null}
+
+
+ );
+};
+
+export default Post;
diff --git a/apps/client/src/shared/ui/post/post.tsx b/apps/client/src/shared/ui/post/post.tsx
deleted file mode 100644
index 9e37f7c5..00000000
--- a/apps/client/src/shared/ui/post/post.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"use client";
-
-import type { Draft } from "@repo/types/draft";
-import type { post } from "@repo/types/evidences";
-import { getCategoryName } from "@repo/utils/handleCategory";
-import { handleState, handleStateColor } from "@repo/utils/handleState";
-import Image from "next/image";
-
-interface PostProps {
- data: post | Draft;
- isExample?: boolean;
- onClick?: () => void;
-}
-
-const Post = ({ data, isExample = false, onClick }: PostProps) => {
- let title = "제목";
- let subTitle = "소제목";
- let imageUri: string | undefined;
- let state = "PENDING";
-
- if ("evidenceType" in data) {
- title = data.evidenceType;
- subTitle = data.categoryName;
- state = data.status;
- } else {
- title = data.title;
- state = data.status;
- if ("author" in data) {
- subTitle = data.author;
- } else if ("categoryName" in data) {
- subTitle = getCategoryName(data.categoryName);
- }
- }
- if ("imageUri" in data) {
- imageUri = data.imageUri;
- }
-
- return (
-
-
- {imageUri == null ? null : (
-
- )}
-
-
-
- {title}
-
- {subTitle}
-
- {!isExample && (
-
- {handleState(state)}
-
- )}
-
-
- );
-};
-
-export default Post;
diff --git a/apps/client/src/views/calculate/ui/index.tsx b/apps/client/src/views/calculate/ui/index.tsx
new file mode 100644
index 00000000..ea4e735e
--- /dev/null
+++ b/apps/client/src/views/calculate/ui/index.tsx
@@ -0,0 +1,16 @@
+import { Calculate } from '@/widgets/calculate/ui'
+
+const CalculateView = () => {
+ return (
+
+ )
+}
+
+export default CalculateView
\ No newline at end of file
diff --git a/apps/client/src/views/changePassword/ui/index.tsx b/apps/client/src/views/changePassword/ui/index.tsx
deleted file mode 100644
index cbe3d191..00000000
--- a/apps/client/src/views/changePassword/ui/index.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-"use client";
-
-import { Button } from "@repo/shared/button";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { HttpStatusCode } from "axios";
-import { useRouter } from "next/navigation";
-import { useCallback, useState } from "react";
-import { useForm } from "react-hook-form";
-import { toast } from "sonner";
-
-import { patchVerifyEmail } from "@/entities/signup/api/patchVerifyEmail";
-import { patchPassword } from "@/shared/api/patchPassword";
-import type {
- ChangePasswordStepForm,
- ChangePasswordProps,
- ChangePW_AuthStepForm,
-} from "@/shared/model/changePWForm";
-import type { HttpError } from "@/shared/types/error";
-import ChangePassword from "@/widgets/changePassword/ui";
-import StepAuthCode from "@/widgets/stepAuthCode/ui";
-import { AuthForm } from "@widgets/auth/ui";
-
-const ChangePasswordView = () => {
- const queryClient = useQueryClient();
- const router = useRouter();
-
- const [step, setStep] = useState("authCode");
- const [isAuthVerifying, setIsAuthVerifying] = useState(false);
- const [verifiedInfo, setVerifiedInfo] = useState<{ email: string } | null>(
- null,
- );
-
- const { mutate: changePWMutate, isPending } = useMutation({
- mutationFn: (form: ChangePasswordProps) => patchPassword(form),
- onSuccess: async (data) => {
- await queryClient.invalidateQueries({
- queryKey: ["auth"],
- exact: false,
- });
- if (data.status === 204) {
- toast.success("비밀번호 변경 성공");
- router.push("/signin");
- }
- },
- onError: (error: HttpError) => {
- if (error.httpStatus === HttpStatusCode.Unauthorized) {
- toast.error("이메일 인증을 먼저 진행해주세요.");
- } else {
- toast.error("비밀번호 변경에 실패했습니다.");
- }
- },
- });
-
- const {
- control: authControl,
- handleSubmit: handleAuthSubmit,
- watch: watchAuth,
- formState: { errors: authErrors },
- } = useForm({
- mode: "onChange",
- defaultValues: {
- email: "",
- authcode: "",
- },
- });
-
- const {
- control,
- handleSubmit,
- formState: { errors: changePWErrors, isValid },
- } = useForm({
- mode: "onChange",
- defaultValues: { password: "", passwordCheck: "" },
- });
-
- const watchedAuthValues = watchAuth();
-
- const isAuthCodeStepValid = Boolean(
- watchedAuthValues.email &&
- /^s\d{5}@gsm\.hs\.kr$/.test(watchedAuthValues.email) &&
- !authErrors.email,
- );
-
- const canProceedToPassword =
- isAuthCodeStepValid &&
- Boolean(
- watchedAuthValues.authcode &&
- watchedAuthValues.authcode.length >= 8 &&
- !authErrors.authcode,
- );
-
- const isPasswordValid = useCallback(
- (data: ChangePasswordStepForm) =>
- Boolean(
- data.password &&
- data.passwordCheck &&
- data.password === data.passwordCheck &&
- !changePWErrors.password &&
- !changePWErrors.passwordCheck,
- ),
- [changePWErrors.password, changePWErrors.passwordCheck],
- );
-
- const handleVerifyEmail = useCallback(
- async (data: ChangePW_AuthStepForm) => {
- if (!canProceedToPassword || isAuthVerifying) return;
-
- try {
- setIsAuthVerifying(true);
- const response = await patchVerifyEmail(Number(data.authcode));
-
- if (response.status === 204) {
- setVerifiedInfo({ email: data.email });
- setStep("password");
- toast.success("이메일 인증이 완료되었습니다.");
- }
- } catch {
- toast.error("인증코드가 일치하지 않습니다.");
- } finally {
- setIsAuthVerifying(false);
- }
- },
- [canProceedToPassword, isAuthVerifying],
- );
-
- const onSubmit = useCallback(
- (data: ChangePasswordStepForm) => {
- if (!verifiedInfo) {
- toast.error("이메일 인증이 필요합니다.");
- setStep("authCode");
- return;
- }
-
- if (step === "password" && isPasswordValid(data) && !isPending) {
- changePWMutate({
- email: verifiedInfo.email,
- password: data.password,
- });
- }
- },
- [changePWMutate, isPasswordValid, isPending, step, verifiedInfo],
- );
-
- const handleAuthCodeSubmit = useCallback(
- (e: React.FormEvent) => {
- void handleAuthSubmit(handleVerifyEmail)(e);
- },
- [handleAuthSubmit, handleVerifyEmail],
- );
-
- const handleChangePassword = useCallback(
- (e: React.FormEvent) => {
- void handleSubmit(onSubmit)(e);
- },
- [handleSubmit, onSubmit],
- );
-
- return (
-
-
- {step === "authCode" ? (
-
-
-
-
-
-
- ) : (
-
-
-
-
-
-
- )}
-
-
- );
-};
-
-export default ChangePasswordView;
diff --git a/apps/client/src/views/detail/index.tsx b/apps/client/src/views/detail/index.tsx
deleted file mode 100644
index f9778f41..00000000
--- a/apps/client/src/views/detail/index.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-"use client";
-
-import { Button } from "@repo/shared/button";
-import ConfirmModal from "@repo/shared/confirmModal";
-import type { Draft } from "@repo/types/draft";
-import type { post } from "@repo/types/evidences";
-import { getCategoryName } from "@repo/utils/handleCategory";
-import Image from "next/image";
-import { useParams, useRouter, useSearchParams } from "next/navigation";
-import { useCallback, useState } from "react";
-import { toast } from "sonner";
-
-import { deletePost } from "@/entities/detail/api/deletePost";
-import { useGetDraft } from "@/entities/posts/lib/useGetDraft";
-import { useGetPosts } from "@/entities/posts/lib/useGetPosts";
-import { useGetCurrentMember } from "@/shared/model/useGetCurrentMember";
-import MockJson from "@shared/mocks/data/evidenceMock.json";
-
-const DetailView = () => {
- const searchParams = useSearchParams();
- const example = searchParams.get("example");
- const draft = searchParams.get("draft");
- const params = useParams();
- const router = useRouter();
- const { id } = params;
- const { data: postsData, isError: isPostsError } = useGetPosts(null);
- const { data: draftsData, isError: isDraftsError } = useGetDraft();
- const { data: studentData, isError: isStudentDataError } =
- useGetCurrentMember();
- const [modalOpen, setModalOpen] = useState(false);
-
- if (isPostsError || isDraftsError) {
- toast.error("게시물을 불러오지 못했습니다.");
- }
-
- if (isStudentDataError) {
- toast.error("회원 정보를 불러오지 못했습니다.");
- }
-
- const posts: post[] = [
- ...(postsData?.data.majorActivityEvidence ?? []),
- ...(postsData?.data.humanitiesActivityEvidence ?? []),
- ...(postsData?.data.readingEvidence ?? []),
- ...(postsData?.data.otherEvidence ?? []),
- ];
-
- const draftPosts: Draft[] = [
- ...(draftsData?.activityEvidences ?? []),
- ...(draftsData?.readingEvidences ?? []),
- ];
-
- const Mock: post[] = MockJson as post[];
-
- let post: post | Draft | undefined;
-
- if (draft === "true") {
- post = draftPosts.find((post) => post.draftId === id);
- } else if (example === "true") {
- post = Mock.find((post) => post.id === Number(id));
- } else {
- post = posts.find((post) => post.id === Number(id));
- }
-
- const handleRevise = useCallback(() => {
- const idString = String(id);
- if (draft === "true") {
- router.push(`/edit/${idString}?draft=${true}`);
- return;
- }
- const exampleQuery =
- example != null && example !== "" ? "?example=true" : "";
-
- router.push(`/edit/${idString}${exampleQuery}`);
- }, [router, id, example, draft]);
-
- const handleBack = useCallback(() => {
- router.back();
- }, [router]);
-
- const handleModalOpen = useCallback(() => {
- setModalOpen(true);
- }, []);
-
- const handleDelete = useCallback(() => {
- void (async () => {
- const res = await deletePost((Number(id)));
- if (res.status === 204) {
- toast.success("게시글이 삭제되었습니다");
- router.push("/posts");
- } else {
- toast.error("게시글 삭제 실패하였습니다");
- }
- })();
- }, [id, router]);
-
- let title = "Title";
- let subTitle = "Author";
- let content = "";
- let imageUri: string | null | undefined;
- let fileUri: string | null | undefined;
-
- if (post) {
- if ("evidenceType" in post) {
- title = post.evidenceType;
- subTitle = `카테고리: ${getCategoryName(post.categoryName)}`;
- content = "자세한 내용은 파일을 확인해주세요.";
- fileUri = post.fileUri;
- } else {
- title = post.title;
- content = post.content;
-
- if ("author" in post && post.author) {
- subTitle = post.author;
- } else if ("categoryName" in post) {
- subTitle = `카테고리: ${getCategoryName(post.categoryName)}`;
- }
-
- if ("imageUri" in post && post.imageUri != null) {
- imageUri = post.imageUri;
- }
- }
- }
-
- return (
-
-
-
-
- {title}
-
-
- {`${studentData?.name ?? "사용자"} · ${subTitle}`}
-
-
-
-
-
- {imageUri == null ? null : (
-
-
-
- )}
-
-
- {subTitle}
-
- {fileUri == null ? (
- content
- ) : (
-
- 증빙 파일 보기
-
- )}
-
-
-
-
- {draft == null && example !== "true" && (
-
- 이 게시글 삭제하기
-
- )}
-
-
- {draft == null && example == null ? (
- <>
-
-
- >
- ) : (
- example == null && (
-
- )
- )}
-
-
- {modalOpen ? (
-
{
- setModalOpen(false);
- },
- }}
- confirm={{
- label: "삭제",
- onClick: () => {
- setModalOpen(false);
- handleDelete();
- },
- }}
- description="정말 이 게시물을 삭제 하시겠습니까?"
- title="게시물 삭제"
- />
- ) : null}
-
- );
-};
-
-export default DetailView;
diff --git a/apps/client/src/views/detail/ui/index.tsx b/apps/client/src/views/detail/ui/index.tsx
new file mode 100644
index 00000000..b21ed822
--- /dev/null
+++ b/apps/client/src/views/detail/ui/index.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import { Button } from "@repo/shared/button";
+import ConfirmModal from "@repo/shared/confirmModal";
+import { usePost } from "@repo/store/postProvider";
+import { getCategoryName } from "@repo/utils/handleCategory";
+import { isActivity, isDraft, isOthers, isReading } from "@repo/utils/handlePost";
+import { useMutation } from "@tanstack/react-query";
+import { HttpStatusCode } from "axios";
+import Image from "next/image";
+import { useParams, useRouter, useSearchParams } from "next/navigation";
+import { useCallback, useState } from "react";
+import { toast } from "sonner";
+
+import { deletePost } from "@/entities/detail/api/deletePost";
+import type { HttpError } from "@/shared/model/error";
+
+const DetailView = () => {
+ const params = useParams();
+ const searchParams = useSearchParams()
+ const router = useRouter();
+
+ const example = searchParams.get("example");
+ const type = searchParams.get("type")
+ const { id } = params;
+
+ const [modalOpen, setModalOpen] = useState(false);
+ const { post } = usePost();
+
+ const { mutate: deletePostMutation } = useMutation({
+ mutationFn: deletePost,
+ onSuccess: (data) => {
+ if (data.status === 204) {
+ toast.success("게시글이 삭제되었습니다");
+ router.replace("/posts");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus == HttpStatusCode.NotFound) {
+ toast.error("해당하는 게시글이 존재하지 않습니다.")
+ } else {
+ toast.error("게시글 삭제를 실패하였습니다.")
+ }
+ }
+ })
+
+ const handleRevise = useCallback(() => {
+ router.push(`/edit/${String(id)}?type=${type}`);
+ }, [id, router, type]);
+
+ const handleBack = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ const handleModalOpen = useCallback(() => {
+ setModalOpen(true);
+ }, []);
+
+ const handleDelete = useCallback(() => {
+ deletePostMutation(Number(id))
+ }, [deletePostMutation, id]);
+
+ if (!post) return 존재하지 않는 게시물입니다.
;
+
+ const title = (() => {
+ if (isActivity(post) || isReading(post)) return post.title;
+ if (isOthers(post)) return post.evidenceType
+ return null;
+ })();
+
+ 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 (
+
+
+
+
+ {imageUri == null ? null : (
+
+
+
+ )}
+
+ {subTitle}
+
+ {isOthers(post) ? (
+
+ 증빙 파일 보기
+
+ ) : (
+ post.content
+ )}
+
+
+
+ {!isDraft(post) && example == null && (
+
+ 이 게시글 삭제하기
+
+ )}
+
+ {(example == null) ? (
+ isDraft(post) ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+
+
+ {modalOpen ? (
+
{
+ setModalOpen(false);
+ },
+ }}
+ confirm={{
+ label: "삭제",
+ onClick: () => {
+ setModalOpen(false);
+ handleDelete();
+ },
+ }}
+ description="정말 이 게시물을 삭제 하시겠습니까?"
+ title="게시물 삭제"
+ />
+ ) : null}
+
+ );
+};
+
+export default DetailView;
diff --git a/apps/client/src/views/edit/index.tsx b/apps/client/src/views/edit/index.tsx
deleted file mode 100644
index b47dcecb..00000000
--- a/apps/client/src/views/edit/index.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"use client";
-
-import type { Draft } from "@repo/types/draft";
-import type { post, Activity } from "@repo/types/evidences";
-import { isActivity, isReading } from "@repo/utils/handlePost";
-import { useParams, useSearchParams } from "next/navigation";
-import { toast } from "sonner";
-
-import { useGetDraft } from "@/entities/posts/lib/useGetDraft";
-import { useGetPosts } from "@/entities/posts/lib/useGetPosts";
-import EditForm from "@/widgets/edit/ui/EditForm";
-
-const EditView = () => {
- const params = useParams();
- const searchParams = useSearchParams();
-
- const { id } = params;
- const isDraft = searchParams.get("draft") === "true";
-
- const { data: postsData, isError: isPostsError } = useGetPosts(null);
- const { data: draftsData, isError: isDraftsError } = useGetDraft();
-
- if (isPostsError || isDraftsError) {
- toast.error("게시물을 불러오지 못했습니다.");
- }
-
- const posts: post[] = [
- ...(postsData?.data.majorActivityEvidence ?? []),
- ...(postsData?.data.humanitiesActivityEvidence ?? []),
- ...(postsData?.data.readingEvidence ?? []),
- ...(postsData?.data.otherEvidence ?? []),
- ];
-
- const draftPosts: Draft[] = [
- ...(draftsData?.activityEvidences ?? []),
- ...(draftsData?.readingEvidences ?? []),
- ];
-
- const post: post | Draft | undefined = isDraft
- ? draftPosts.find((p) => String(p.draftId) === id)
- : posts.find((p) => p.id === Number(id));
-
- if (!post) {
- return 게시물을 찾을 수 없습니다.
;
- }
-
- let type: "major" | "humanities" | "reading" | "others";
-
- if ("draftId" in post) {
- if ("author" in post) {
- type = "reading";
- } else if ("categoryName" in post && post.categoryName === "MAJOR") {
- type = "major";
- } else {
- type = "humanities";
- }
- } else {
- const isMajorActivity =
- isActivity(post) &&
- postsData?.data.majorActivityEvidence.some(
- (p: Activity) => p.id === post.id,
- );
-
- const isHumanitiesActivity =
- isActivity(post) &&
- postsData?.data.humanitiesActivityEvidence.some(
- (p: Activity) => p.id === post.id,
- );
-
- if (isMajorActivity ?? false) {
- type = "major";
- } else if (isHumanitiesActivity ?? false) {
- type = "humanities";
- } else if (isReading(post)) {
- type = "reading";
- } else {
- type = "others";
- }
- }
-
- return ;
-};
-
-export default EditView;
diff --git a/apps/client/src/views/edit/ui/index.tsx b/apps/client/src/views/edit/ui/index.tsx
new file mode 100644
index 00000000..ab94b9b6
--- /dev/null
+++ b/apps/client/src/views/edit/ui/index.tsx
@@ -0,0 +1,7 @@
+import EditForm from "@/widgets/edit/ui";
+
+const EditView = () => {
+ return ;
+};
+
+export default EditView;
diff --git a/apps/client/src/views/main/ui/index.tsx b/apps/client/src/views/main/ui/index.tsx
index a2c51c8b..338160c4 100644
--- a/apps/client/src/views/main/ui/index.tsx
+++ b/apps/client/src/views/main/ui/index.tsx
@@ -9,7 +9,7 @@ import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import ShowSignin from "@/entities/main/ui/showSignin";
-import { useGetCurrentMember } from "@/shared/model/useGetCurrentMember";
+import { useGetCurrentMember } from "@/shared/lib/useGetCurrentMember";
import { getCertification } from "@entities/main/api/getCertification";
import MainDropdown from "@entities/main/ui/dropdown";
import { ShowInformation } from "@entities/main/ui/showInformation";
@@ -28,9 +28,9 @@ const MainView = () => {
setAccessToken(token);
}, []);
- const { data: currentUser } = useGetCurrentMember();
+ const { data: currentUser, refetch: currentUserRefetch } = useGetCurrentMember();
- const { data: certification, refetch } = useQuery({
+ const { data: certification, refetch: certificationRefetch } = useQuery({
queryKey: ["certifications"],
queryFn: getCertification,
enabled: !(accessToken == null),
@@ -39,26 +39,22 @@ const MainView = () => {
const handleHoverDropdown = useCallback(
(category: string) => () => {
setHoverTab(category);
- },
- [],
- );
+ }, []);
const handleLeaveDropdown = useCallback(() => {
setHoverTab(null);
}, []);
- const handleOpenModal = useCallback(
- (modalname: ModalType) => () => {
- setType(modalname);
- setShow(true);
- },
- [],
- );
+ const handleOpenModal = useCallback((modalname: ModalType) => () => {
+ setType(modalname);
+ setShow(true);
+ }, []);
const handleCloseModal = useCallback(() => {
setShow(false);
- void refetch();
- }, [refetch]);
+ void currentUserRefetch();
+ void certificationRefetch();
+ }, [certificationRefetch, currentUserRefetch]);
return (
@@ -156,28 +152,26 @@ const MainView = () => {
-
+
-
-
- {accessToken == null ? (
-
- 로그인 후 확인가능합니다.
-
- ) : (certification &&
- certification.data.certificates.length > 0 ? (
- certification.data.certificates.map((v, i) => (
-
- ))
- ) : (
-
- 등록된 자격증이 존재하지 않습니다.
-
- ))}
-
+
+ {accessToken == null ? (
+
+ 로그인 후 확인가능합니다.
+
+ ) : (certification &&
+ certification.certificates.length > 0 ? (
+ certification.certificates.map((v, i) => (
+
+ ))
+ ) : (
+
+ 등록된 자격증이 존재하지 않습니다.
+
+ ))}
diff --git a/apps/client/src/views/posts/api/getDraft.ts b/apps/client/src/views/posts/api/getDraft.ts
deleted file mode 100644
index 701322fb..00000000
--- a/apps/client/src/views/posts/api/getDraft.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import instance from "@repo/api/axios";
-import type { DraftResponse } from "@repo/types/draft";
-
-export const getDraft = async () => {
- const response = await instance.get("/evidence/current/draft");
- return response.data;
-};
diff --git a/apps/client/src/views/posts/api/getPosts.ts b/apps/client/src/views/posts/api/getPosts.ts
deleted file mode 100644
index 4482c91c..00000000
--- a/apps/client/src/views/posts/api/getPosts.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import instance from "@repo/api/axios";
-import type { EvidenceResponse, EvidenceType } from "@repo/types/evidences";
-
-export const getPosts = async (type: EvidenceType | null) => {
- return await instance.get(
- `/evidence/current${type === null ? "" : "?type=" + type}`,
- );
-};
diff --git a/apps/client/src/views/posts/ui/myPosts.tsx b/apps/client/src/views/posts/ui/index.tsx
similarity index 100%
rename from apps/client/src/views/posts/ui/myPosts.tsx
rename to apps/client/src/views/posts/ui/index.tsx
diff --git a/apps/client/src/views/signin/ui/index.tsx b/apps/client/src/views/signin/ui/index.tsx
index 1432a96e..63e6b45a 100644
--- a/apps/client/src/views/signin/ui/index.tsx
+++ b/apps/client/src/views/signin/ui/index.tsx
@@ -1,30 +1,33 @@
"use client";
+import { postSignin } from "@/entities/signin/api/postSignin";
+import type { HttpError } from "@/shared/model/error";
+import type { SigninFormProps } from "@/shared/model/signin";
import { Button } from "@repo/shared/button";
+import { EyeClose } from "@repo/shared/eyeClose";
+import { EyeOpen } from "@repo/shared/eyeOpen";
import { Input } from "@repo/shared/input";
import { InputContainer } from "@repo/shared/inputContainer";
import { setCookie } from "@repo/utils/setCookie";
import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { AuthForm } from "@widgets/auth/ui";
import { HttpStatusCode } from "axios";
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
-import { postSignin } from "@/entities/signin/api/postSignin";
-import type { HttpError } from "@/shared/types/error";
-import type { SigninFormProps } from "@shared/model/AuthForm";
-import { AuthForm } from "@widgets/auth/ui";
const SigninView = () => {
const queryClient = useQueryClient();
const router = useRouter();
+ const [showPassword, setShowPassword] = useState(false)
const {
control,
handleSubmit,
- formState: { isValid },
+ formState: { isValid, errors },
} = useForm({
mode: "onChange",
defaultValues: { email: "", password: "" },
@@ -33,11 +36,15 @@ const SigninView = () => {
const { mutate: signinMutate } = useMutation({
mutationFn: (form: SigninFormProps) => postSignin(form),
onSuccess: async (data) => {
- if (data.accessToken) {
- setCookie("accessToken", data.accessToken, 1);
+ if (data.status == 200) {
+ toast.success("로그인되었습니다.")
}
- if (data.refreshToken) {
- setCookie("refreshToken", data.refreshToken);
+
+ if (data.data.accessToken) {
+ setCookie("accessToken", data.data.accessToken, 1);
+ }
+ if (data.data.refreshToken) {
+ setCookie("refreshToken", data.data.refreshToken);
}
await queryClient.invalidateQueries({
@@ -46,7 +53,6 @@ const SigninView = () => {
});
router.push("/");
- return data;
},
onError: (error: HttpError) => {
if (error.httpStatus == HttpStatusCode.Unauthorized) {
@@ -55,7 +61,6 @@ const SigninView = () => {
toast.error("회원가입되지 않은 계정입니다.");
router.push("signup");
}
- throw error;
},
});
@@ -73,6 +78,10 @@ const SigninView = () => {
[handleSubmit, onSubmit],
);
+ const handleShowPassword = useCallback(() => {
+ setShowPassword(!showPassword)
+ }, [showPassword])
+
return (
@@ -82,9 +91,8 @@ const SigninView = () => {
onSubmit={handleFormSubmit}
>
{
+ const [step, setStep] = useState("authCode");
const queryClient = useQueryClient();
const router = useRouter();
- const [step, setStep] = useState("authCode");
- const [isAuthVerifying, setIsAuthVerifying] = useState(false);
- const [verifiedInfo, setVerifiedInfo] = useState<{
- name: string;
- email: string;
- } | null>(null);
+ const methods = useForm({
+ defaultValues: {
+ name: '',
+ email: '',
+ authcode: '',
+ password: '',
+ passwordCheck: ''
+ },
+ mode: "onChange"
+ })
- const { mutate: signupMutate, isPending } = useMutation({
+ const { mutate: signupMutate } = useMutation({
mutationFn: (form: SignupFormProps) => postSignup(form),
onSuccess: async (data) => {
await queryClient.invalidateQueries({
@@ -54,159 +57,35 @@ const SignupView = () => {
},
});
- const {
- control: authControl,
- handleSubmit: handleAuthSubmit,
- watch: watchAuth,
- formState: { errors: authErrors },
- } = useForm({
- mode: "onChange",
- defaultValues: {
- name: "",
- email: "",
- authcode: "",
- },
- });
-
- const {
- control: signupControl,
- handleSubmit: handleSignupSubmit,
- formState: { errors: signupErrors, isValid },
- } = useForm({
- mode: "onChange",
- defaultValues: {
- password: "",
- passwordCheck: "",
- },
- });
-
- const watchedAuthValues = watchAuth();
-
- const isAuthCodeStepValid = Boolean(
- watchedAuthValues.name &&
- watchedAuthValues.email &&
- /^s\d{5}@gsm\.hs\.kr$/.test(watchedAuthValues.email) &&
- !authErrors.name &&
- !authErrors.email,
- );
-
- const canProceedToPassword =
- isAuthCodeStepValid &&
- Boolean(
- watchedAuthValues.authcode &&
- watchedAuthValues.authcode.length >= 8 &&
- !authErrors.authcode,
- );
-
- const isPasswordValid = useCallback(
- (data: SignupStepForm) =>
- Boolean(
- data.password &&
- data.passwordCheck &&
- data.password === data.passwordCheck &&
- !signupErrors.password &&
- !signupErrors.passwordCheck,
- ),
- [signupErrors.password, signupErrors.passwordCheck],
- );
-
- const handleVerifyEmail = useCallback(
- async (data: AuthStepForm) => {
- if (!canProceedToPassword || isAuthVerifying) return;
-
- try {
- setIsAuthVerifying(true);
- const response = await patchVerifyEmail(Number(data.authcode));
+ const handlePostSignup = useCallback((form: StepAuthCodeForm & StepPasswordForm) => {
+ signupMutate({
+ name: form.name,
+ email: form.email,
+ password: form.password
+ })
+ }, [signupMutate])
- if (response.status === 204) {
- setVerifiedInfo({ name: data.name, email: data.email });
- setStep("password");
- toast.success("이메일 인증이 완료되었습니다.");
- }
- } catch {
- toast.error("인증코드가 일치하지 않습니다.");
- } finally {
- setIsAuthVerifying(false);
- }
- },
- [canProceedToPassword, isAuthVerifying],
- );
-
- const onSubmit = useCallback(
- (data: SignupStepForm) => {
- if (!verifiedInfo) {
- toast.error("이메일 인증이 필요합니다.");
- setStep("authCode");
- return;
- }
-
- if (step === "password" && isPasswordValid(data) && !isPending) {
- signupMutate({
- email: verifiedInfo.email,
- name: verifiedInfo.name,
- password: data.password,
- });
- }
- },
- [verifiedInfo, setStep, step, isPasswordValid, isPending, signupMutate],
- );
-
- const handleAuthCodeSubmit = useCallback(
- (e: React.FormEvent) => {
- void handleAuthSubmit(handleVerifyEmail)(e);
- },
- [handleAuthSubmit, handleVerifyEmail],
- );
-
- const handlePasswordSubmit = useCallback(
- (e: React.FormEvent) => {
- void handleSignupSubmit(onSubmit)(e);
- },
- [handleSignupSubmit, onSubmit],
- );
+ const onSubmit = useCallback(async () => {
+ await methods.handleSubmit(handlePostSignup)()
+ }, [handlePostSignup, methods])
return (
- {step === "authCode" ? (
-
-
-
-
-
-
- ) : (
-
-
-
-
-
-
- )}
+
+
+ {step === "authCode" &&
+
+
+
+ }
+ {step === "password" &&
+
+
+
+ }
+
+
);
diff --git a/apps/client/src/widgets/calculate/model/category.ts b/apps/client/src/widgets/calculate/model/category.ts
index e8e21de8..8fb9b1af 100644
--- a/apps/client/src/widgets/calculate/model/category.ts
+++ b/apps/client/src/widgets/calculate/model/category.ts
@@ -125,72 +125,72 @@ export const humanCategoryOptions = [
name: "교내인성영역관련수상",
score: "50점",
max_number: "최대 4회",
- send: "humanities-award_career-humanity-in_school",
+ send: "HUMANITIES-AWARD_CAREER-HUMANITY-IN_SCHOOL",
id: 5,
},
{
name: "교외인성영역관련수상",
score: "50점",
max_number: "최대 4회",
- send: "humanities-award_career-humanity-out_school",
+ send: "HUMANITIES-AWARD_CAREER-HUMANITY-OUT_SCHOOL",
id: 5,
},
{
name: "독서마라톤 거북이코스",
score: "40점",
- send: "humanities-book-bookmarathon-turtle",
+ send: "HUMANITIES-BOOK-BOOKMARATHON-TURTLE",
id: 6,
},
{
name: "독서마라톤 악어코스",
score: "70점",
- send: "humanities-book-bookmarathon-crocodile",
+ send: "HUMANITIES-BOOK-BOOKMARATHON-CROCODILE",
id: 6,
},
{
name: "독서마라톤 토끼코스이상",
score: "100점",
- send: "humanities-book-bookmarathon-over_rabbit",
+ send: "HUMANITIES-BOOK-BOOKMARATHON-OVER_RABBIT",
id: 6,
},
{
name: "봉사활동",
score: "시간당 5점",
max_number: "최대 40시간",
- send: "humanities-service-activity",
+ send: "HUMANITIES-SERVICE-ACTIVITY",
id: 8,
},
{
name: "봉사동아리",
score: "학기당 50점",
max_number: "최대 2",
- send: "humanities-service-club",
+ send: "HUMANITIES-SERVICE-CLUB",
id: 9,
},
{
name: "한자자격증",
score: "50점 (4급 이상)",
- send: "humanities-certificate-chinese_character",
+ send: "HUMANITIES-CERTIFICATE-CHINESE_CHARACTER",
id: 10,
},
{
name: "한국사",
score: "50점 (3급 이상)",
- send: "humanities-certificate-korean_history",
+ send: "HUMANITIES-CERTIFICATE-KOREAN_HISTORY",
id: 11,
},
{
name: "자기주도적 활동",
score: "25점",
max_number: "최대 8회",
- send: "humanities-activities-self_directed_activities",
+ send: "HUMANITIES-ACTIVITIES-SELF_DIRECTED_ACTIVITIES",
id: 12,
},
{
name: "뉴로우S",
score: "5점 (1일 1회로 인정)",
max_number: "최대 40",
- send: "humanities-activities-newrrow_s",
+ send: "HUMANITIES-ACTIVITIES-NEWRROW_S",
id: 13,
},
];
@@ -199,55 +199,55 @@ export const foreignCategoryOptions = [
{
name: "TOEIC 점수",
score: "500점",
- send: "foreign_lang-test-toeic-score",
+ send: "FOREIGN_LANG-TEST-TOEIC-SCORE",
id: 14,
},
{
- name: " TOEFL 점수",
+ name: "TOEFL 점수",
score: "500점",
- send: "foreign_lang-test-toefl-score",
+ send: "FOREIGN_LANG-TEST-TOEFL-SCORE",
id: 14,
},
{
name: "TEPS 점수",
score: "500점",
- send: "foreign_lang-test-teps-score",
+ send: "FOREIGN_LANG-TEST-TEPS-SCORE",
id: 14,
},
{
name: "TOEIC SPEAKING level",
score: "500점",
- send: "foreign_lang-test-toeic_speaking-level",
+ send: "FOREIGN_LANG-TEST-TOEIC_SPEAKING-LEVEL",
id: 14,
},
{
name: "OPIC 등급",
score: "500점",
- send: "foreign_lang-test-opic-grade",
+ send: "FOREIGN_LANG-TEST-OPIC-GRADE",
id: 14,
},
{
name: "JPT 점수",
score: "500점",
- send: "foreign_lang-test-jpt-score",
+ send: "FOREIGN_LANG-TEST-JPT-SCORE",
id: 14,
},
{
name: "CPT 점수",
score: "500점",
- send: "foreign_lang-test-cpt-score",
+ send: "FOREIGN_LANG-TEST-CPT-SCORE",
id: 14,
},
{
name: "HSK 급수",
score: "500점",
- send: "foreign_lang-test-hsk-grade",
+ send: "FOREIGN_LANG-TEST-HSK-GRADE",
id: 14,
},
{
name: "토사관 참여",
score: "100점",
- send: "foreign_lang-attendance_toeic_academy-status",
+ send: "FOREIGN_LANG-ATTENDANCE_TOEIC_ACADEMY-STATUS",
id: 15,
},
];
diff --git a/apps/client/src/widgets/calculate/ui/index.tsx b/apps/client/src/widgets/calculate/ui/index.tsx
index 5d3a82f6..d5536221 100644
--- a/apps/client/src/widgets/calculate/ui/index.tsx
+++ b/apps/client/src/widgets/calculate/ui/index.tsx
@@ -20,18 +20,13 @@ export const Calculate = () => {
const [bookCount, setBookCount] = useState(0);
const [totalScore, setTotalScore] = useState(0);
- const [selectedOptions, setSelectedOptions] = useState<
- Record
- >({
+ const [selectedOptions, setSelectedOptions] = useState>({
인성: null,
전공: null,
외국어: null,
});
- const [categoryCounts, setCategoryCounts] = useState>(
- {},
- );
-
+ const [categoryCounts, setCategoryCounts] = useState>({});
const [idCounts, setIdCounts] = useState>({});
const { control } = useForm();
@@ -44,9 +39,9 @@ export const Calculate = () => {
const selectedId = selected.id;
const currentSendCount = categoryCounts[selected.send] ?? 0;
const currentIdCount = idCounts[selectedId] ?? 0;
- const max =
- Number.parseInt(selected.max_number?.replace(/[^0-9]/g, "") ?? "") || 1;
+ const max = Number.parseInt(selected.max_number?.replace(/[^0-9]/g, "") ?? "") || 1;
const score = Number.parseInt(selected.score?.match(/\d+/)?.[0] ?? "0");
+
if (currentIdCount < max) {
setCategoryCounts((prev) => ({
...prev,
@@ -71,6 +66,7 @@ export const Calculate = () => {
const score = Number.parseInt(selected.score?.match(/\d+/)?.[0] ?? "0");
const currentSendCount = categoryCounts[selected.send] ?? 0;
const currentIdCount = idCounts[selected.id] ?? 0;
+
if (currentSendCount > 0 && currentIdCount > 0) {
setCategoryCounts((prev) => ({
...prev,
@@ -93,13 +89,6 @@ export const Calculate = () => {
[],
);
- const handleBookMinus = useCallback(() => {
- if (bookCount > 0) {
- setBookCount((prev) => prev - 1);
- setTotalScore((prev) => prev - 10);
- }
- }, [bookCount]);
-
const handleBookPlus = useCallback(() => {
if (bookCount < 10) {
setBookCount((prev) => prev + 1);
@@ -107,6 +96,13 @@ export const Calculate = () => {
}
}, [bookCount]);
+ const handleBookMinus = useCallback(() => {
+ if (bookCount > 0) {
+ setBookCount((prev) => prev - 1);
+ setTotalScore((prev) => prev - 10);
+ }
+ }, [bookCount]);
+
return (
diff --git a/apps/client/src/widgets/changePassword/ui/index.tsx b/apps/client/src/widgets/changePassword/ui/index.tsx
deleted file mode 100644
index a5d57406..00000000
--- a/apps/client/src/widgets/changePassword/ui/index.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Input } from "@repo/shared/input";
-import { InputContainer } from "@repo/shared/inputContainer";
-import React from "react";
-import type { Control } from "react-hook-form";
-
-import type { ChangePasswordStepForm } from "@/shared/model/changePWForm";
-
-export default function ChangePassword({
- control,
-}: {
- control: Control
;
-}) {
- return (
- <>
-
-
-
-
-
- value === formValues.password || "비밀번호가 일치하지 않습니다.",
- }}
- type="password"
- />
-
- >
- );
-}
diff --git a/apps/client/src/widgets/edit/api/patchActivity.ts b/apps/client/src/widgets/edit/api/patchActivity.ts
new file mode 100644
index 00000000..be50412f
--- /dev/null
+++ b/apps/client/src/widgets/edit/api/patchActivity.ts
@@ -0,0 +1,38 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+export const patchMajorActivity = async (
+ evidenceId: number,
+ activity: FormData
+): Promise => {
+ try {
+ const response = await instance.patch(
+ `evidence/major/${evidenceId}`,
+ activity
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "전공 영역 수정 실패";
+ }
+ throw error;
+ }
+};
+
+export const patchHumanitiesActivity = async (
+ evidenceId: number,
+ activity: FormData
+): Promise => {
+ try {
+ const response = await instance.patch(
+ `evidence/humanities/${evidenceId}`,
+ activity
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "인성 영역 수정 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/edit/api/patchOthers.ts b/apps/client/src/widgets/edit/api/patchOthers.ts
new file mode 100644
index 00000000..64f6bba6
--- /dev/null
+++ b/apps/client/src/widgets/edit/api/patchOthers.ts
@@ -0,0 +1,20 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosError, type AxiosResponse } from "axios";
+
+export const patchOthers = async (
+ evidenceId: number,
+ formData: FormData
+): Promise => {
+ try {
+ const response = await instance.patch(
+ `evidence/other/${evidenceId}`,
+ formData
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "기타 영역 수정 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/edit/api/patchReading.ts b/apps/client/src/widgets/edit/api/patchReading.ts
new file mode 100644
index 00000000..2fae23e9
--- /dev/null
+++ b/apps/client/src/widgets/edit/api/patchReading.ts
@@ -0,0 +1,22 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+import type { Book } from "@/widgets/write/model/book";
+
+export const patchReading = async (
+ evidenceId: number,
+ bookData: Book
+): Promise => {
+ try {
+ const response = await instance.patch(
+ `evidence/reading/${evidenceId}`,
+ bookData
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "독서 영역 수정 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/edit/api/patchScoring.ts b/apps/client/src/widgets/edit/api/patchScoring.ts
new file mode 100644
index 00000000..be083289
--- /dev/null
+++ b/apps/client/src/widgets/edit/api/patchScoring.ts
@@ -0,0 +1,20 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+export const patchScoring = async (
+ evidenceId: number,
+ formData: FormData
+): Promise => {
+ try {
+ const response = await instance.patch(
+ `evidence/scoring/${evidenceId}`,
+ formData
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "외국어 영역 수정 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/edit/lib/getDefaultValues.ts b/apps/client/src/widgets/edit/lib/getDefaultValues.ts
index 8528709b..0895cf2f 100644
--- a/apps/client/src/widgets/edit/lib/getDefaultValues.ts
+++ b/apps/client/src/widgets/edit/lib/getDefaultValues.ts
@@ -1,41 +1,51 @@
-import type { Activity, Reading, Others } from "@repo/types/evidences";
+import type { DraftType } from "@repo/types/draft";
+import type { PostType } from "@repo/types/evidences";
+import { isActivity, isReading } from "@repo/utils/handlePost";
-import { getEditConfig } from "../model/config";
-import type { FormValues } from "../types/types";
+import type { ConfigType } from "@/shared/model/config";
+import type { FormValues } from "@/shared/model/formValues";
+import {
+ HumanitiesOptions,
+ MajorOptions,
+} from "@/widgets/write/model/category";
export const getDefaultValues = (
- type: "major" | "humanities" | "reading" | "others",
- post: Activity | Reading | Others
+ type: ConfigType,
+ post: PostType | DraftType | undefined
): Partial => {
- const config = getEditConfig(type);
+ if (!post) return {};
+ switch (type) {
+ case "major":
+ case "humanities": {
+ if (isActivity(post)) {
+ const categoryOptions =
+ type === "major" ? MajorOptions : HumanitiesOptions;
+ return {
+ title: post.title,
+ content: post.content,
+ categoryName: categoryOptions.find(
+ (option) => post.categoryName === option.send
+ ),
+ };
+ }
+ break;
+ }
- if (type === "reading" && "author" in post) {
- return {
- title: post.title,
- author: post.author,
- page: String(post.page),
- content: post.content,
- };
- }
-
- if (
- (type === "major" || type === "humanities") &&
- "title" in post &&
- "content" in post &&
- "categoryName" in post
- ) {
- const defaultCategory =
- config.categoryOptions?.find(
- (option) => option.send === post.categoryName
- ) ?? config.categoryOptions?.[0];
+ case "reading": {
+ if (isReading(post)) {
+ return {
+ title: post.title,
+ author: post.author,
+ page: post.page,
+ content: post.content,
+ };
+ }
+ break;
+ }
- return {
- title: post.title,
- content: post.content,
- categoryName: defaultCategory,
- file: undefined,
- };
+ case "others": {
+ throw new Error("외국어 영역의 수정은 지원되지 않습니다,");
+ }
}
-
return {};
};
diff --git a/apps/client/src/widgets/edit/model/config.ts b/apps/client/src/widgets/edit/model/editConfig.ts
similarity index 60%
rename from apps/client/src/widgets/edit/model/config.ts
rename to apps/client/src/widgets/edit/model/editConfig.ts
index 9764f10d..6ca68a71 100644
--- a/apps/client/src/widgets/edit/model/config.ts
+++ b/apps/client/src/widgets/edit/model/editConfig.ts
@@ -1,26 +1,30 @@
+import type { AxiosResponse } from "axios";
+
+import type { ConfigType } from "@/shared/model/config";
+import type { FormValues, Option } from "@/shared/model/formValues";
+import {
+ MajorOptions,
+ HumanitiesOptions,
+} from "@/widgets/write/model/category";
+
import {
- updateMajorActivity,
- updateHumanitiesActivity,
-} from "@/shared/api/updateActivity";
-import { updateReading } from "@/shared/api/updateReading";
-import { majorCategoryOptions } from "@/widgets/calculate/model/category";
-import type { FormValues } from "@/widgets/edit/types/types";
-import { CharacterCategory } from "@/widgets/write/model/category";
+ patchMajorActivity,
+ patchHumanitiesActivity,
+} from "../api/patchActivity";
+import { patchReading } from "../api/patchReading";
interface Config {
title: string;
- categoryOptions?: { name: string; send: string }[];
- onSubmit: (data: FormValues, id: number) => Promise;
+ categoryOptions?: Option[];
+ onSubmit: (data: FormValues, id: number) => Promise;
}
-export const getEditConfig = (
- type: "major" | "humanities" | "reading" | "others",
-): Config => {
+export const getEditConfig = (type: ConfigType): Config => {
switch (type) {
case "major": {
return {
title: "전공 영역 수정",
- categoryOptions: majorCategoryOptions,
+ categoryOptions: MajorOptions,
onSubmit: async (data: FormValues, id: number) => {
const formData = new FormData();
if (data.file) {
@@ -31,14 +35,14 @@ export const getEditConfig = (
formData.append("content", data.content || "");
formData.append("activityType", "MAJOR");
- await updateMajorActivity(id, formData);
+ return await patchMajorActivity(id, formData);
},
};
}
case "humanities": {
return {
title: "인성 영역 수정",
- categoryOptions: CharacterCategory,
+ categoryOptions: HumanitiesOptions,
onSubmit: async (data: FormValues, id: number) => {
const formData = new FormData();
if (data.file) {
@@ -49,7 +53,7 @@ export const getEditConfig = (
formData.append("content", data.content || "");
formData.append("activityType", "HUMANITIES");
- await updateHumanitiesActivity(id, formData);
+ return await patchHumanitiesActivity(id, formData);
},
};
}
@@ -63,17 +67,12 @@ export const getEditConfig = (
page: Number(data.page) || 0,
content: data.content || "",
};
- await updateReading(id, bookData);
+ return await patchReading(id, bookData);
},
};
}
case "others": {
- return {
- title: "기타 증빙 수정",
- onSubmit: async () => {
- // No submission logic implemented for 'others' category yet.
- },
- };
+ throw new Error("외국어 영역의 수정은 지원되지 않습니다,");
}
}
};
diff --git a/apps/client/src/widgets/edit/types/types.ts b/apps/client/src/widgets/edit/types/types.ts
deleted file mode 100644
index 27172eda..00000000
--- a/apps/client/src/widgets/edit/types/types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { Draft } from "@repo/types/draft";
-import type { post } from "@repo/types/evidences";
-
-export interface Option {
- name: string;
- send: string;
-}
-
-export interface FormValues {
- title: string;
- content: string;
- categoryName?: Option;
- file?: File;
- author?: string;
- page?: string;
- value?: string | number;
- draftId?: string;
-}
-
-export interface EditFormProps {
- type: "major" | "humanities" | "reading" | "others" | "foreign";
- post: post | Draft;
-}
diff --git a/apps/client/src/widgets/edit/ui/EditForm.tsx b/apps/client/src/widgets/edit/ui/EditForm.tsx
deleted file mode 100644
index 09f66987..00000000
--- a/apps/client/src/widgets/edit/ui/EditForm.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-"use client";
-
-import { Button } from "@repo/shared/button";
-import { Input } from "@repo/shared/input";
-import { InputContainer } from "@repo/shared/inputContainer";
-import type { Activity, Others, Reading } from "@repo/types/evidences";
-import { useRouter, useSearchParams } from "next/navigation";
-import { useCallback } from "react";
-import { Controller, useForm, useWatch } from "react-hook-form";
-import { toast } from "sonner";
-
-import { Dropdown, File, Textarea } from "@/shared/ui";
-import { getDefaultValues } from "@/widgets/edit/lib/getDefaultValues";
-import { getEditConfig } from "@/widgets/edit/model/config";
-import type {
- EditFormProps,
- FormValues,
- Option,
-} from "@/widgets/edit/types/types";
-import { getWriteConfig } from "@/widgets/write/model/writeConfig";
-
-const EditForm = ({ type, post }: EditFormProps) => {
- const searchParams = useSearchParams();
- const isDraft = Boolean(searchParams.get("draft"));
- const router = useRouter();
-
- const config = getEditConfig(type as "major" | "humanities" | "reading" | "others");
- const draftConfig = getWriteConfig(type as "major" | "humanities" | "reading" | "others");
-
- const {
- handleSubmit,
- control,
- formState: { isValid },
- } = useForm({
- mode: "onChange",
- defaultValues: getDefaultValues(
- type as "major" | "humanities" | "reading" | "others",
- post as Activity | Reading | Others,
- ),
- });
-
- const file = useWatch({ control, name: "file" });
-
- const handleFormSubmit = useCallback(
- async (data: FormValues) => {
- try {
- if (isDraft && "draftId" in post) {
- await draftConfig.onSubmit({ ...data, draftId: post.draftId }, "submit");
- toast.success("작성이 완료되었습니다.");
- router.back();
- return;
- } else if (!isDraft && "id" in post) {
- await config.onSubmit(data, Number(post.id));
- toast.success("수정이 완료되었습니다.");
- router.back();
- }
- } catch {
- toast.error("수정에 실패했습니다.");
- }
- },
- [config, draftConfig, isDraft, post, router],
- );
-
- const handleReviseSubmit = useCallback(
- (e: React.FormEvent) => {
- void handleSubmit(handleFormSubmit)(e);
- },
- [handleSubmit, handleFormSubmit],
- );
-
- const handleBack = useCallback(() => {
- router.back();
- }, [router]);
-
- if (type === "others") {
- return (
-
-
-
- {config.title}
-
-
-
-
- 기타 증빙 자료는 현재 수정 기능을 지원하지 않습니다.
-
- {"categoryName" in post && (
-
- 카테고리: {post.categoryName}
-
- )}
- {"evidenceType" in post && (
-
- 유형: {post.evidenceType}
-
- )}
-
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
- {config.title}
-
-
- {(type === "major" || type === "humanities") &&
- config.categoryOptions ? (
-
- control={control}
- name="categoryName"
- // eslint-disable-next-line react/jsx-no-bind
- render={({ field: { value, onChange, ...field } }) => (
-
- )}
- rules={{
- required: "카테고리를 선택해주세요.",
- }}
- />
- ) : null}
-
-
-
- control={control}
- name="title"
- rules={{
- required: "제목을 입력해주세요.",
- }}
- />
-
-
- {type === "reading" && (
- <>
-
-
- control={control}
- name="author"
- rules={{
- required: "저자를 입력해주세요.",
- }}
- />
-
-
-
- control={control}
- name="page"
- rules={{
- required: "페이지를 입력해주세요.",
- }}
- />
-
- >
- )}
-
-
- control={control}
- name="content"
- // eslint-disable-next-line react/jsx-no-bind
- render={({ field: { value, onChange, ...field } }) => (
-
- )}
- rules={{
- required: "내용을 입력해주세요.",
- minLength: {
- value: type === "reading" ? 600 : (file == null ? 400 : 200),
- message:
- type === "reading"
- ? "600자 이상 입력해주세요."
- : (file == null
- ? "내용을 400자 이상 입력해주세요."
- : "내용을 200자 이상 입력해주세요."),
- },
- }}
- />
-
- {(type === "major" || type === "humanities") && (
-
- control={control}
- name="file"
- // eslint-disable-next-line react/jsx-no-bind
- render={({ field: { value, onChange, ...field } }) => (
-
- )}
- />
- )}
-
-
-
-
-
-
-
-
- );
-};
-
-export default EditForm;
diff --git a/apps/client/src/widgets/edit/ui/index.tsx b/apps/client/src/widgets/edit/ui/index.tsx
new file mode 100644
index 00000000..2d57d58d
--- /dev/null
+++ b/apps/client/src/widgets/edit/ui/index.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import { Button } from "@repo/shared/button";
+import { Input } from "@repo/shared/input";
+import { InputContainer } from "@repo/shared/inputContainer";
+import { usePost } from "@repo/store/postProvider";
+import { isActivity, isDraft, isReading } from "@repo/utils/handlePost";
+import { useMutation } from "@tanstack/react-query";
+import { HttpStatusCode } from "axios";
+import { useParams, useRouter, useSearchParams } from "next/navigation";
+import { useCallback, useMemo } from "react";
+import { Controller, useForm, useWatch } from "react-hook-form";
+import { toast } from "sonner";
+
+import type { ConfigType } from "@/shared/model/config";
+import type { HttpError } from "@/shared/model/error";
+import type { FormValues, Option } from "@/shared/model/formValues";
+import { Dropdown, File, Textarea } from "@/shared/ui";
+import { getDefaultValues } from "@/widgets/edit/lib/getDefaultValues";
+import { getEditConfig } from "@/widgets/edit/model/editConfig";
+import { getWriteConfig } from "@/widgets/write/model/writeConfig";
+
+const EditForm = () => {
+ const params = useParams();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const { id } = params;
+ const typeParam = searchParams.get('type');
+ const { post } = usePost();
+
+ const type: ConfigType = useMemo(() => {
+ if (!post) {
+ throw new Error('Post 데이터가 필요합니다.');
+ }
+
+ if (typeParam != null) {
+ switch (typeParam) {
+ case 'MAJOR': {
+ return 'major';
+ }
+ case 'HUMANITIES': {
+ return 'humanities';
+ }
+ case 'READING': {
+ return 'reading';
+ }
+ case 'FOREIGN_LANGUAGE': {
+ return 'others';
+ }
+ case 'DRAFT': {
+ if (isDraft(post)) {
+ if (isActivity(post)) {
+ return post.activityType.toLowerCase() as ConfigType;
+ }
+ if (isReading(post)) {
+ return 'reading';
+ }
+ }
+ throw new Error('요청하신 타입과 실제 데이터의 타입이 다릅니다.');
+ }
+ default: {
+ throw new Error(`지원되지 않는 타입입니다: ${typeParam}`);
+ }
+ }
+ }
+ throw new Error('typeParam이 없습니다.');
+ }, [post, typeParam]);
+
+ const editConfig = getEditConfig(type);
+ const writeConfig = getWriteConfig(type);
+
+ const { mutate: editMutatation } = useMutation({
+ mutationFn: (data: FormValues) => editConfig.onSubmit(data, Number(id)),
+ onSuccess: (data) => {
+ if (data.status === 201) {
+ toast.success("증빙자료를 성공적으로 저장하였습니다.")
+ router.push("/");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus === HttpStatusCode.NotFound) {
+ toast.error("해당 증빙 자료가 존재하지 않습니다.");
+ } else if (error.httpStatus === HttpStatusCode.UnprocessableEntity) {
+ toast.error("더 이상 증빙 자료를 추가할 수 없습니다.");
+ } else {
+ toast.error("증빙 자료 추가에 실패했습니다.");
+ }
+ }
+ })
+
+ const { mutate: draftMutation } = useMutation({
+ mutationFn: (data: FormValues) => writeConfig.onSubmit({ ...data, draftId: String(id) }, "submit"),
+ onSuccess: (data) => {
+ if (data.status === 201) {
+ toast.success("증빙자료를 성공적으로 저장하였습니다.")
+ router.push("/");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus === HttpStatusCode.NotFound) {
+ toast.error("해당 증빙 자료가 존재하지 않습니다.");
+ } else if (error.httpStatus === HttpStatusCode.UnprocessableEntity) {
+ toast.error("더 이상 증빙 자료를 추가할 수 없습니다.");
+ } else {
+ toast.error("증빙 자료 추가에 실패했습니다.");
+ }
+ }
+ })
+
+ const {
+ handleSubmit,
+ control,
+ formState: { isValid, errors },
+ } = useForm({
+ mode: "onChange",
+ defaultValues: getDefaultValues(type, post),
+ });
+
+ const file = useWatch({ control, name: "file" });
+
+ const handleEditSubmit = useCallback((data: FormValues) => {
+ editMutatation(data)
+ router.replace("/posts")
+ }, [router, editMutatation]);
+
+ const handleDraftSubmit = useCallback((data: FormValues) => {
+ draftMutation(data)
+ router.replace("/posts")
+ }, [draftMutation, router]);
+
+ const handleReviseSubmit = useCallback((e: React.FormEvent) => {
+ if (post && isDraft(post)) {
+ void handleSubmit(handleDraftSubmit)(e);
+ } else {
+ void handleSubmit(handleEditSubmit)(e);
+ }
+ }, [handleDraftSubmit, handleEditSubmit, handleSubmit, post]);
+
+ const handleBack = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ return (
+
+
+
+ {writeConfig.title}
+
+
+ {(type === "major" || type === "humanities") &&
+ writeConfig.categoryOptions ? (
+
+ control={control}
+ name="categoryName"
+ // eslint-disable-next-line react/jsx-no-bind
+ render={({ field: { value, onChange, ...field } }) => (
+
+ )}
+ rules={{
+ required: "카테고리를 선택해주세요.",
+ }}
+ />
+ ) : null}
+
+
+
+ control={control}
+ name="title"
+ rules={{
+ required: "제목을 입력해주세요.",
+ }}
+ />
+
+
+ {type === "reading" && (
+ <>
+
+
+ control={control}
+ name="author"
+ rules={{
+ required: "저자를 입력해주세요.",
+ }}
+ />
+
+
+
+ control={control}
+ name="page"
+ rules={{
+ required: "페이지를 입력해주세요.",
+ }}
+ />
+
+ >
+ )}
+
+
+ control={control}
+ name="content"
+ // eslint-disable-next-line react/jsx-no-bind
+ render={({ field: { value, onChange, ...field } }) => (
+
+ )}
+ rules={{
+ required: "내용을 입력해주세요.",
+ minLength: {
+ value: type === "reading" ? 600 : (file == null ? 400 : 200),
+ message:
+ type === "reading"
+ ? "600자 이상 입력해주세요."
+ : (file == null
+ ? "내용을 400자 이상 입력해주세요."
+ : "내용을 200자 이상 입력해주세요."),
+ },
+ }}
+ />
+
+ {(type === "major" || type === "humanities") && (
+
+ control={control}
+ name="file"
+ // eslint-disable-next-line react/jsx-no-bind
+ render={({ field: { value, onChange, ...field } }) => (
+
+ )}
+ />
+ )}
+
+
+ {post && isDraft(post) ?
+
+ :
+
+ }
+
+
+
+
+
+ );
+};
+
+export default EditForm;
\ No newline at end of file
diff --git a/apps/client/src/widgets/example/ui/index.tsx b/apps/client/src/widgets/example/ui/index.tsx
index 942aae85..66d5c212 100644
--- a/apps/client/src/widgets/example/ui/index.tsx
+++ b/apps/client/src/widgets/example/ui/index.tsx
@@ -1,7 +1,7 @@
"use client";
import { usePost } from "@repo/store/postProvider";
-import type { post } from "@repo/types/evidences";
+import type { PostType } from "@repo/types/evidences";
import { useRouter } from "next/navigation";
import { useCallback } from "react";
@@ -10,25 +10,29 @@ import Mock from "@shared/mocks/data/evidenceMock.json";
export default function ExampleWidget() {
const { setPost } = usePost();
- const R = useRouter();
+ const router = useRouter();
- const handleExamplePost = useCallback(
- (data: post) => () => {
- setPost(data);
- R.push(`/detail/${data.id}?example=${true}`);
- },
- [R, setPost],
- );
+ const handleRoute = useCallback((mock: PostType) => () => {
+ setPost(mock);
+ router.push(`/detail/${mock.id}?example=${true}`);
+ }, [router, setPost]);
+
+ const mockPosts: PostType[] = [
+ ...(Mock.majorActivityEvidence),
+ ...(Mock.humanitiesActivityEvidence),
+ ...(Mock.readingEvidence),
+ ...(Mock.otherEvidence),
+ ] as PostType[];
return (
- {Mock.map((data) => {
+ {mockPosts.map((mock) => {
return (
);
})}
diff --git a/apps/client/src/widgets/header/ui/index.tsx b/apps/client/src/widgets/header/ui/index.tsx
index ef88f946..131ecd6c 100644
--- a/apps/client/src/widgets/header/ui/index.tsx
+++ b/apps/client/src/widgets/header/ui/index.tsx
@@ -4,13 +4,22 @@ import ConfirmModal from "@repo/shared/confirmModal";
import TextLogo from "@repo/shared/textLogo";
import { deleteCookie } from "@repo/utils/deleteCookie";
import { getCookie } from "@repo/utils/getCookie";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { HttpStatusCode } from "axios";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
+import { Form, FormProvider, useForm } from "react-hook-form";
+import { toast } from "sonner";
+import { patchPassword } from "@/shared/api/patchPassword";
import { Close } from "@/shared/asset/svg/close";
import { Hamburger } from "@/shared/asset/svg/hamburger";
-
+import type { ChangePassword_StepAuthCodeForm, ChangePasswordProps } from "@/shared/model/changePassword";
+import type { HttpError } from "@/shared/model/error";
+import type { StepPasswordForm } from "@/shared/model/signup";
+import { StepAuthcode } from "@/widgets/stepAuthcode/ui";
+import { StepPassword } from "@/widgets/stepPassword/ui";
interface HeaderType {
href: string;
label?: string;
@@ -21,18 +30,60 @@ const Header = () => {
const pathname = usePathname();
const [accessToken, setAccessToken] = useState
(null);
const [menuOpen, setMenuOpen] = useState(false);
- const [modalOpen, setModalOpen] = useState(false);
+ const [modalOpen, setModalOpen] = useState<"confirmModal" | "changePassword" | null>(null);
const router = useRouter();
+ const queryClient = useQueryClient();
+
+ const [step, setStep] = useState("authCode");
useEffect(() => {
const token = getCookie("accessToken");
setAccessToken(token);
}, [pathname]);
- const handleModalOpen = useCallback(() => {
- setModalOpen(true);
+ const { mutate: changePasswordMutate } = useMutation({
+ mutationFn: (form: ChangePasswordProps) => patchPassword(form),
+ onSuccess: async (data) => {
+ await queryClient.invalidateQueries({
+ queryKey: ["auth"],
+ exact: false,
+ });
+ if (data.status === 204) {
+ toast.success("비밀번호 변경 성공");
+ router.push("/signin");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus === HttpStatusCode.Unauthorized) {
+ toast.error("이메일 인증을 먼저 진행해주세요.");
+ } else {
+ toast.error("비밀번호 변경에 실패했습니다.");
+ }
+ },
+ });
+
+ const methods = useForm({
+ mode: "onChange",
+ defaultValues: {
+ email: "",
+ authcode: "",
+ password: "",
+ passwordCheck: ""
+ },
+ });
+
+ const handleConfirmModalOpen = useCallback(() => {
+ setModalOpen("confirmModal");
}, []);
+ const handleChangePasswordModalOpen = useCallback(() => {
+ setModalOpen("changePassword");
+ }, []);
+
+ const handleModalClose = useCallback(() => {
+ setModalOpen(null)
+ }, [])
+
const handleSignoutConfirm = useCallback(() => {
deleteCookie("accessToken");
deleteCookie("refreshToken");
@@ -43,10 +94,21 @@ const Header = () => {
setMenuOpen((prev) => !prev);
}, [setMenuOpen]);
- const handleStopPropagation = useCallback((e: React.MouseEvent) => {
+ const handleStopPropagation = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
}, []);
+ const handleChangePassword = useCallback((form: ChangePassword_StepAuthCodeForm & StepPasswordForm) => {
+ changePasswordMutate({
+ email: form.email,
+ password: form.password
+ })
+ }, [changePasswordMutate])
+
+ const onSubmit = useCallback(async () => {
+ await methods.handleSubmit(handleChangePassword)()
+ }, [handleChangePassword, methods])
+
const header = [
{
href: "/calculate",
@@ -65,12 +127,7 @@ const Header = () => {
},
];
- if (
- pathname === "/signin" ||
- pathname === "/signup" ||
- pathname === "/changePassword"
- )
- return null;
+ if (pathname === "/signin" || pathname === "/signup") return null;
return (
<>
@@ -82,7 +139,7 @@ const Header = () => {
{menuOpen ? : }
-
+
{header.map((item: HeaderType) => (
{
))}
{accessToken === null ? null : (
<>
-
-
- 비밀번호 변경
-
+
+ 비밀번호 변경
로그아웃
@@ -115,7 +170,7 @@ const Header = () => {
-
@@ -139,36 +194,35 @@ const Header = () => {
))}
{accessToken === null ? null : (
<>
-
-
- 비밀번호 변경
-
+
+ 비밀번호 변경
로그아웃
>
)}
-
- {modalOpen ? (
+
+
+ {modalOpen == "confirmModal" ? (
{
- setModalOpen(false);
+ setModalOpen(null);
},
}}
confirm={{
label: "로그아웃",
onClick: () => {
- setModalOpen(false);
+ setModalOpen(null);
handleSignoutConfirm();
},
}}
@@ -176,6 +230,34 @@ const Header = () => {
title="로그아웃"
/>
) : null}
+
+ {modalOpen == "changePassword" ? (
+
+
+
+
+ 비밀번호 변경
+ {step === "authCode" &&
+
+
+
+ }
+ {step == "password" &&
+
+
+
+ }
+
+
+
+
+ ) : null}
>
);
};
diff --git a/apps/client/src/widgets/main/api/sendCertification.ts b/apps/client/src/widgets/main/api/sendCertification.ts
index df6c2d49..6b2610a2 100644
--- a/apps/client/src/widgets/main/api/sendCertification.ts
+++ b/apps/client/src/widgets/main/api/sendCertification.ts
@@ -1,10 +1,10 @@
import instance from "@repo/api/axios";
import { isAxiosError, type AxiosResponse } from "axios";
-import type { Certification } from "../model/certification";
+import type { CertificationRequest } from "@/shared/model/certification";
export const sendCertification = async (
- data: Certification
+ data: CertificationRequest
): Promise => {
try {
const formData = new FormData();
diff --git a/apps/client/src/widgets/main/api/sendEvidence.ts b/apps/client/src/widgets/main/api/sendEvidence.ts
index ed6e4160..40c6a670 100644
--- a/apps/client/src/widgets/main/api/sendEvidence.ts
+++ b/apps/client/src/widgets/main/api/sendEvidence.ts
@@ -1,10 +1,21 @@
import instance from "@repo/api/axios";
+import { isAxiosError } from "axios";
-import type { Evidence } from "../model/evidence";
+interface EvidenceProps {
+ categoryName: string;
+ file: File;
+}
-export const sendEvidence = async (data: Evidence) => {
- const formData = new FormData();
- formData.append("file", data.file);
- formData.append("categoryName", data.option.send);
- return await instance.post("/evidence/current/other", formData);
+export const sendEvidence = async (data: EvidenceProps) => {
+ try {
+ const formData = new FormData();
+ formData.append("file", data.file);
+ formData.append("categoryName", data.categoryName);
+ return await instance.post("/evidence/current/other", formData);
+ } catch (error) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "기타증빙자료 등록 실패";
+ }
+ throw error;
+ }
};
diff --git a/apps/client/src/widgets/main/model/certification.ts b/apps/client/src/widgets/main/model/certification.ts
deleted file mode 100644
index f3bb6a2c..00000000
--- a/apps/client/src/widgets/main/model/certification.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface Certification {
- name: string;
- file: File;
- acquisitionDate: string;
-}
diff --git a/apps/client/src/widgets/main/model/evidence.ts b/apps/client/src/widgets/main/model/evidence.ts
index d453f8a7..c78418ba 100644
--- a/apps/client/src/widgets/main/model/evidence.ts
+++ b/apps/client/src/widgets/main/model/evidence.ts
@@ -1,7 +1,7 @@
export interface Evidence {
categoryName: string;
file: File;
- acquisitionDate?: string;
+ acquisitionDate?: Date;
value?: number;
option: { send: string; name: string };
}
diff --git a/apps/client/src/widgets/main/model/options.ts b/apps/client/src/widgets/main/model/options.ts
index e494dfe5..3546371a 100644
--- a/apps/client/src/widgets/main/model/options.ts
+++ b/apps/client/src/widgets/main/model/options.ts
@@ -9,8 +9,14 @@ export const options = [
];
export const bookOption = [
- { name: "거북이 코스", send: "HUMANITIES-READING-READ_A_THON-TURTLE" },
- { name: "악어 코스", send: "HUMANITIES-READING-READ_A_THON-CROCODILE" },
+ {
+ name: "거북이 코스",
+ send: "HUMANITIES-READING-READ_A_THON-TURTLE",
+ },
+ {
+ name: "악어 코스",
+ send: "HUMANITIES-READING-READ_A_THON-CROCODILE",
+ },
{
name: "토끼 코스 이상",
send: "HUMANITIES-READING-READ_A_THON-RABBIT_OVER",
diff --git a/apps/client/src/widgets/main/ui/modal.tsx b/apps/client/src/widgets/main/ui/modal.tsx
index b0eb616a..a82cb220 100644
--- a/apps/client/src/widgets/main/ui/modal.tsx
+++ b/apps/client/src/widgets/main/ui/modal.tsx
@@ -8,8 +8,8 @@ import React, { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { toast } from "sonner";
-import type { HttpError } from "@/shared/types/error";
-import { FixScore } from "@shared/api/fixScore";
+import type { HttpError } from "@/shared/model/error";
+import { PostScore } from "@shared/api/postScore";
import Dropdown from "@shared/ui/dropdown";
import File from "@shared/ui/file";
@@ -29,8 +29,20 @@ const Modal = ({ onClose, type }: ModalProps) => {
const {
handleSubmit,
control,
- formState: { isValid },
- } = useForm({ mode: "onChange" });
+ formState: { isValid, errors },
+ } = useForm({
+ defaultValues: {
+ categoryName: "",
+ file: undefined,
+ acquisitionDate: new Date,
+ value: 0,
+ option: {
+ send: "",
+ name: ""
+ }
+ },
+ mode: "onChange"
+ });
const handleCloseModal = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
@@ -53,14 +65,17 @@ const Modal = ({ onClose, type }: ModalProps) => {
});
}
case "TOPCIT": {
- return await FixScore({
+ return await PostScore({
categoryName: "MAJOR-TOPCIT_SCORE",
file: data.file,
value: Number(data.value),
});
}
case "READ_A_THON": {
- return await sendEvidence(data);
+ return await sendEvidence({
+ categoryName: data.option.send,
+ file: data.file
+ });
}
case "HUMANITY": {
return await sendCertification({
@@ -126,11 +141,13 @@ const Modal = ({ onClose, type }: ModalProps) => {
onClick={handleStopPropagation}
>
- {type === "TOPCIT"
- ? "TOPCIT"
- : (type === "READ_A_THON"
- ? "독서로"
- : "자격증")}
+ {type === "TOPCIT" ?
+ "TOPCIT" :
+ (type === "READ_A_THON" ?
+ "독서로" :
+ "자격증"
+ )
+ }
{
{...field}
/>
)}
- rules={{ required: true }}
+ rules={{
+ required: "카테고리를 선택해주세요."
+ }}
/>
) : (
)}
{(type === "CERTIFICATE" || type === "HUMANITY") && (
-
+
@@ -177,8 +207,10 @@ const Modal = ({ onClose, type }: ModalProps) => {
control={control}
name="file"
// eslint-disable-next-line react/jsx-no-bind
- render={({ field }) => }
- rules={{ required: true }}
+ render={({ field }) => }
+ rules={{
+ required: "취득 여부를 구분할 수 있는 파일을 첨부해주세요."
+ }}
/>
diff --git a/apps/client/src/widgets/posts/ui/index.tsx b/apps/client/src/widgets/posts/ui/index.tsx
index 185c14ae..68e7132b 100644
--- a/apps/client/src/widgets/posts/ui/index.tsx
+++ b/apps/client/src/widgets/posts/ui/index.tsx
@@ -2,46 +2,35 @@
import { Button } from "@repo/shared/button";
import { usePost } from "@repo/store/postProvider";
-import type { Draft } from "@repo/types/draft";
-import type { EvidenceResponse, post } from "@repo/types/evidences";
+import type { DraftType } from "@repo/types/draft";
+import type { PostType, PostResponse } from "@repo/types/evidences";
+import { isDraft } from "@repo/utils/handlePost";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { toast } from "sonner";
-import { useGetDraft } from "@/entities/posts/lib/useGetDraft";
-import { useGetPosts } from "@/entities/posts/lib/useGetPosts";
import Search from "@/entities/posts/ui/search";
+import { useGetDraft } from "@/shared/lib/useGetDraft";
+import { useGetPosts } from "@/shared/lib/useGetPosts"
import { Post } from "@/shared/ui";
import type { CategoryType } from "../model/category";
export default function PostsWidget() {
- const R = useRouter();
- const [result, setResult] = useState
();
+ const router = useRouter();
+ const [result, setResult] = useState();
const [search, setSearch] = useState("");
const [categoryName, setCategoryName] = useState("READING");
- const { data: postsData, isError: isPostsError } = useGetPosts(categoryName);
- const { data: draftsData, isError: isDraftsError } = useGetDraft();
+ const { posts, isError: isPostsError } = useGetPosts(categoryName);
+ const { drafts, isError: isDraftsError } = useGetDraft(categoryName);
const { setPost } = usePost();
if (isPostsError || isDraftsError) {
toast.error("게시물을 불러오지 못했습니다.");
}
- const posts: post[] = [
- ...(postsData?.data.majorActivityEvidence ?? []),
- ...(postsData?.data.humanitiesActivityEvidence ?? []),
- ...(postsData?.data.readingEvidence ?? []),
- ...(postsData?.data.otherEvidence ?? []),
- ];
-
- const draftPosts: Draft[] = [
- ...(draftsData?.activityEvidences ?? []),
- ...(draftsData?.readingEvidences ?? []),
- ];
-
- const resultPosts: post[] = [
+ const resultPosts: PostType[] = [
...(result?.majorActivityEvidence ?? []),
...(result?.humanitiesActivityEvidence ?? []),
...(result?.readingEvidence ?? []),
@@ -56,35 +45,29 @@ export default function PostsWidget() {
{ label: "임시저장", value: "DRAFT" },
];
- const handleCategory = useCallback(
- (value: CategoryType) => () => {
- setCategoryName(value);
- },
- [],
- );
-
- const handleRoute = useCallback(
- (post: post | Draft) => () => {
- setPost(post);
- if ("draftId" in post) {
- R.push(`/detail/${post.draftId}?draft=${true}`);
- return;
- }
- R.push(`/detail/${post.id}`);
- },
- [R, setPost],
- );
-
- let displayedPosts: (post | Draft)[] = [];
+ let displayedPosts: (PostType | DraftType)[] = [];
if (search.trim().length > 0 && resultPosts.length > 0) {
displayedPosts = resultPosts;
} else if (categoryName === "DRAFT") {
- displayedPosts = draftPosts;
+ displayedPosts = drafts;
} else {
displayedPosts = posts;
}
+ const handleCategory = useCallback((value: CategoryType) => () => {
+ setCategoryName(value);
+ }, []);
+
+ const handleRoute = useCallback((post: PostType | DraftType) => () => {
+ setPost(post);
+ if (isDraft(post)) {
+ router.push(`/detail/${post.draftId}?type=${categoryName}`);
+ } else {
+ router.push(`/detail/${post.id}?type=${categoryName}`);
+ }
+ }, [categoryName, router, setPost]);
+
return (
(
))}
diff --git a/apps/client/src/widgets/stepAuthCode/ui/index.tsx b/apps/client/src/widgets/stepAuthCode/ui/index.tsx
deleted file mode 100644
index ccd56d2e..00000000
--- a/apps/client/src/widgets/stepAuthCode/ui/index.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { Button } from "@repo/shared/button";
-import { Input } from "@repo/shared/input";
-import { InputContainer } from "@repo/shared/inputContainer";
-import React, { useCallback, useState } from "react";
-import { type Control, useWatch } from "react-hook-form";
-import { toast } from "sonner";
-
-import type { ChangePW_AuthStepForm } from "@/shared/model/changePWForm";
-import { postSendEmail } from "@entities/signup/api/postSendEmail";
-import type { AuthStepForm } from "@shared/model/AuthForm";
-
-interface StepAuthCodeBaseProps {
- isAuthButtonActive: boolean;
-}
-
-interface StepAuthCodeWithNameProps extends StepAuthCodeBaseProps {
- control: Control;
- hasName: true;
-}
-
-interface StepAuthCodeWithoutNameProps extends StepAuthCodeBaseProps {
- control: Control;
- hasName?: false;
-}
-
-type StepAuthCodeProps =
- | StepAuthCodeWithNameProps
- | StepAuthCodeWithoutNameProps;
-
-export default function StepAuthCode(props: StepAuthCodeProps) {
- const { control, isAuthButtonActive, hasName = false } = props;
- const [isLoading, setIsLoading] = useState(false);
- const [show, setShow] = useState(false);
-
- const email = useWatch({
- control: control as Control,
- name: "email",
- });
-
- const handleAuthButtonClick = useCallback(() => {
- const sendEmail = async () => {
- if (isAuthButtonActive && !isLoading) {
- try {
- setIsLoading(true);
-
- await postSendEmail(email);
- toast.success("인증번호가 전송되었습니다.");
- setShow(true);
- } catch (error) {
- toast.error(String(error));
- } finally {
- setIsLoading(false);
- }
- }
- };
- void sendEmail();
- }, [email, isAuthButtonActive, isLoading]);
-
- return (
- <>
- {hasName ? (
-
- }
- name="name"
- rules={{
- required: "이름을 필수로 입력해야 합니다.",
- }}
- />
-
- ) : null}
-
-
-
- }
- name="email"
- rules={{
- required: "이메일을 필수로 입력해야 합니다.",
- pattern: {
- value: /^s\d{5}@gsm\.hs\.kr$/,
- message: "@gsm.hs.kr 학교 이메일을 입력해주세요",
- },
- }}
- />
-
-
-
-
-
- }
- name="authcode"
- rules={{
- required: "인증번호를 필수로 입력해야 합니다.",
- }}
- />
-
-
- {show ? (
-
- 인증 코드를 찾을 수 없나요? 스팸메일함을 확인해
- 주세요.
-
- ) : null}
- >
- );
-}
diff --git a/apps/client/src/widgets/stepAuthcode/ui/index.tsx b/apps/client/src/widgets/stepAuthcode/ui/index.tsx
new file mode 100644
index 00000000..54f2b0b4
--- /dev/null
+++ b/apps/client/src/widgets/stepAuthcode/ui/index.tsx
@@ -0,0 +1,126 @@
+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 React, { useCallback } from "react";
+import { useFormContext } from "react-hook-form";
+import { toast } from "sonner";
+
+import { patchVerifyEmail } from "@/entities/signup/api/patchVerifyEmail";
+import type { ChangePassword_StepAuthCodeForm } from "@/shared/model/changePassword";
+import type { HttpError } from "@/shared/model/error";
+import { postSendEmail } from "@entities/signup/api/postSendEmail";
+import type { StepAuthCodeForm } from "@shared/model/signup";
+
+const StepAuthcode = ({ setStep }: { setStep: (step: string) => void }) => {
+ const { control, watch, formState: { errors, isValid } } = useFormContext()
+ const email = watch("email")
+ const authcode = watch("authcode")
+
+ const { mutate: sendEmailMutate, isPending: isSendEmailPending, isSuccess: isSendEmailSuccess } = useMutation({
+ mutationFn: (email: string) => postSendEmail(email),
+ onSuccess: (data) => {
+ if (data.status == 204) {
+ toast.success("인증번호가 전송되었습니다.");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus == HttpStatusCode.BadRequest) {
+ toast.error("인증번호 전송을 실패하였습니다.")
+ }
+ }
+ })
+
+ const { mutate: verifyEmailMutate, isSuccess: isVerifyEmail } = useMutation({
+ mutationFn: (authcode: string) => patchVerifyEmail(Number(authcode)),
+ onSuccess: (data) => {
+ if (data.status == 204) {
+ setStep("password");
+ toast.success("이메일 인증이 완료되었습니다.");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus == HttpStatusCode.BadRequest) {
+ toast.error("올바르지 않은 인증코드입니다.")
+ } else if (error.httpStatus == HttpStatusCode.Unauthorized) {
+ toast.error("유효하지 않거나 만료된 인증코드입니다.")
+ }
+ }
+ })
+
+ const handlePostSendEmail = useCallback(() => {
+ sendEmailMutate(email)
+ }, [email, sendEmailMutate])
+
+ const handlePatchVerifyEmail = useCallback(() => {
+ verifyEmailMutate(authcode)
+ if (isVerifyEmail && isValid) {
+ setStep("password")
+ }
+ }, [authcode, isValid, isVerifyEmail, setStep, verifyEmailMutate])
+
+ return (
+ <>
+ {"name" in control._defaultValues &&
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+ {isSendEmailSuccess ?
+
+ 인증 코드를 찾을 수 없나요? 스팸메일함을 확인해주세요.
+
: null}
+ >
+ );
+}
+
+export { StepAuthcode }
\ No newline at end of file
diff --git a/apps/client/src/widgets/stepPassword/ui/index.tsx b/apps/client/src/widgets/stepPassword/ui/index.tsx
index 9df953b8..d3b1a792 100644
--- a/apps/client/src/widgets/stepPassword/ui/index.tsx
+++ b/apps/client/src/widgets/stepPassword/ui/index.tsx
@@ -1,44 +1,94 @@
+import type { StepPasswordForm } from "@/shared/model/signup";
+import { Button } from "@repo/shared/button";
+import { EyeClose } from "@repo/shared/eyeClose";
+import { EyeOpen } from "@repo/shared/eyeOpen";
import { Input } from "@repo/shared/input";
import { InputContainer } from "@repo/shared/inputContainer";
-import React from "react";
-import type { Control } from "react-hook-form";
+import React, { useCallback, useState } from "react";
+import { useFormContext } from "react-hook-form";
-import type { SignupStepForm } from "@shared/model/AuthForm";
-export default function StepPassword({
- control,
-}: {
- control: Control;
-}) {
+const StepPassword = ({ isHeader = false }: { isHeader?: boolean }) => {
+ const { control, watch, formState: { errors, isValid } } = useFormContext()
+ const password = watch("password")
+
+ const [showPassword, setShowPassword] = useState(false)
+ const [showPasswordCheck, setShowPasswordCheck] = useState(false)
+
+ const handleShowPassword = useCallback(() => {
+ setShowPassword(!showPassword)
+ }, [showPassword])
+
+ const handleShowPasswordCheck = useCallback(() => {
+ setShowPasswordCheck(!showPasswordCheck)
+ }, [showPasswordCheck])
return (
<>
-
-
+
+
+
+
+ {showPassword ? : }
+
+
-
-
- value === formValues.password || "비밀번호가 일치하지 않습니다.",
- }}
- type="password"
- />
+
+
+ passwordCheck === password || "비밀번호가 일치하지 않습니다.",
+ }}
+ type={showPasswordCheck ? "text" : "password"}
+ />
+
+ {showPasswordCheck ? : }
+
+
+
>
);
}
+
+export { StepPassword }
\ No newline at end of file
diff --git a/apps/client/src/widgets/write/api/postActivity.ts b/apps/client/src/widgets/write/api/postActivity.ts
new file mode 100644
index 00000000..0bdb9b03
--- /dev/null
+++ b/apps/client/src/widgets/write/api/postActivity.ts
@@ -0,0 +1,16 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+export const postActivity = async (
+ activity: FormData
+): Promise => {
+ try {
+ const response = await instance.post("evidence/current/activity", activity);
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "활동 영역 저장 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/write/api/postActivityDraft.ts b/apps/client/src/widgets/write/api/postActivityDraft.ts
new file mode 100644
index 00000000..93b3c9be
--- /dev/null
+++ b/apps/client/src/widgets/write/api/postActivityDraft.ts
@@ -0,0 +1,19 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+export const postActivityDraft = async (
+ activity: FormData
+): Promise => {
+ try {
+ const response = await instance.post(
+ "/evidence/current/draft/activity",
+ activity
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "활동 영역 임시 저장 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/write/api/postReading.ts b/apps/client/src/widgets/write/api/postReading.ts
new file mode 100644
index 00000000..de5c17f5
--- /dev/null
+++ b/apps/client/src/widgets/write/api/postReading.ts
@@ -0,0 +1,16 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+import type { Book } from "../model/book";
+
+export const postReading = async (bookData: Book): Promise => {
+ try {
+ const response = await instance.post("evidence/current/reading", bookData);
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "독서 영역 저장 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/write/api/postReadingDraft.ts b/apps/client/src/widgets/write/api/postReadingDraft.ts
new file mode 100644
index 00000000..4c287469
--- /dev/null
+++ b/apps/client/src/widgets/write/api/postReadingDraft.ts
@@ -0,0 +1,19 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+import type { Book } from "../model/book";
+
+export const postReadingDraft = async (data: Book): Promise => {
+ try {
+ const response = await instance.post(
+ "/evidence/current/draft/reading",
+ data
+ );
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "독서 영역 임시 저장 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/write/api/postScoring.ts b/apps/client/src/widgets/write/api/postScoring.ts
new file mode 100644
index 00000000..3f9cbed7
--- /dev/null
+++ b/apps/client/src/widgets/write/api/postScoring.ts
@@ -0,0 +1,16 @@
+import instance from "@repo/api/axios";
+import { isAxiosError, type AxiosResponse } from "axios";
+
+export const postScoring = async (
+ formData: FormData
+): Promise => {
+ try {
+ const response = await instance.post("/evidence/current/scoring", formData);
+ return response;
+ } catch (error: unknown) {
+ if (isAxiosError(error) && error.response) {
+ throw error.response.data ?? "외국어 영역 저장 실패";
+ }
+ throw error;
+ }
+};
diff --git a/apps/client/src/widgets/write/api/saveBookDraft.ts b/apps/client/src/widgets/write/api/saveBookDraft.ts
deleted file mode 100644
index a0a285da..00000000
--- a/apps/client/src/widgets/write/api/saveBookDraft.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import instance from "@repo/api/axios";
-
-import type { Book } from "../model/book";
-
-export const saveBookDraft = async (data: Book) => {
- return await instance.post("/evidence/current/draft/reading", data);
-};
diff --git a/apps/client/src/widgets/write/api/saveDraft.ts b/apps/client/src/widgets/write/api/saveDraft.ts
deleted file mode 100644
index 58bbb9dd..00000000
--- a/apps/client/src/widgets/write/api/saveDraft.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
-
-export const saveDraft = async (
- activity: FormData,
-): Promise => {
- try {
- const res = await instance.post(
- "/evidence/current/draft/activity",
- activity,
- );
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
diff --git a/apps/client/src/widgets/write/api/sendActivity.ts b/apps/client/src/widgets/write/api/sendActivity.ts
deleted file mode 100644
index a09a6b5c..00000000
--- a/apps/client/src/widgets/write/api/sendActivity.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import instance from "@repo/api/axios";
-import type { AxiosError, AxiosResponse } from "axios";
-
-export const sendActivity = async (
- activity: FormData,
-): Promise => {
- try {
- const res = await instance.post("evidence/current/activity", activity);
- return res;
- } catch (error) {
- return error as AxiosError;
- }
-};
diff --git a/apps/client/src/widgets/write/api/sendBook.ts b/apps/client/src/widgets/write/api/sendBook.ts
deleted file mode 100644
index c8218946..00000000
--- a/apps/client/src/widgets/write/api/sendBook.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import instance from "@repo/api/axios";
-
-import type { Book } from "../model/book";
-
-export const sendBook = async (bookData: Book) => {
- await instance.post("evidence/current/reading", bookData);
-};
diff --git a/apps/client/src/widgets/write/lib/handleBookSubmit.ts b/apps/client/src/widgets/write/lib/handleBookSubmit.ts
deleted file mode 100644
index 41708dc4..00000000
--- a/apps/client/src/widgets/write/lib/handleBookSubmit.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { toast } from "sonner";
-
-import { saveBookDraft } from "../api/saveBookDraft";
-import { sendBook } from "../api/sendBook";
-import type { Book } from "../model/book";
-
-export const handleSubmitBook = async (
- data: Book,
- submitType: "submit" | "draft",
-) => {
- try {
- await (submitType === "draft" ? saveBookDraft(data) : sendBook(data));
- toast.success(submitType === "draft" ? "임시저장 완료" : "제출 완료");
- return true;
- } catch {
- toast.error(submitType === "draft" ? "임시저장 실패" : "제출 실패");
- return false;
- }
-};
diff --git a/apps/client/src/widgets/write/lib/handleSubmitActivity.ts b/apps/client/src/widgets/write/lib/handleSubmitActivity.ts
index 73dc5eba..fe630fa6 100644
--- a/apps/client/src/widgets/write/lib/handleSubmitActivity.ts
+++ b/apps/client/src/widgets/write/lib/handleSubmitActivity.ts
@@ -1,20 +1,11 @@
-import { toast } from "sonner";
-
-import { saveDraft } from "../api/saveDraft";
-import { sendActivity } from "../api/sendActivity";
+import { postActivity } from "../api/postActivity";
+import { postActivityDraft } from "../api/postActivityDraft";
export const handleSubmitActivity = async (
submitType: "submit" | "draft",
- formData: FormData,
+ formData: FormData
) => {
- try {
- await (submitType === "draft"
- ? saveDraft(formData)
- : sendActivity(formData));
- toast.success(submitType === "draft" ? "임시저장 완료" : "제출 완료");
- return true;
- } catch {
- toast.error(submitType === "draft" ? "임시저장 실패" : "제출 실패");
- return false;
- }
+ return await (submitType === "draft"
+ ? postActivityDraft(formData)
+ : postActivity(formData));
};
diff --git a/apps/client/src/widgets/write/lib/handleSubmitReading.ts b/apps/client/src/widgets/write/lib/handleSubmitReading.ts
new file mode 100644
index 00000000..92c78750
--- /dev/null
+++ b/apps/client/src/widgets/write/lib/handleSubmitReading.ts
@@ -0,0 +1,12 @@
+import { postReading } from "../api/postReading";
+import { postReadingDraft } from "../api/postReadingDraft";
+import type { Book } from "../model/book";
+
+export const handleSubmitReading = async (
+ data: Book,
+ submitType: "submit" | "draft"
+) => {
+ return await (submitType === "draft"
+ ? postReadingDraft(data)
+ : postReading(data));
+};
diff --git a/apps/client/src/widgets/write/model/category.ts b/apps/client/src/widgets/write/model/category.ts
index 1e3b8c16..96bde2f2 100644
--- a/apps/client/src/widgets/write/model/category.ts
+++ b/apps/client/src/widgets/write/model/category.ts
@@ -1,14 +1,112 @@
-export const CharacterCategory = [
+export const MajorOptions = [
{
- name: "교내 인성영역 관련수상",
- send: "HUMANITIES-AWARD_CAREER-HUMANITY-IN_SCHOOL",
+ name: "공문을 통한 전공분야 대회 수상",
+ send: "MAJOR-AWARD_CAREER-OUT_SCHOOL-OFFICIAL",
},
{
- name: "교외 인성영역 관련수상",
- send: "HUMANITIES-AWARD_CAREER-HUMANITY-OUT_SCHOOL",
+ name: "전공분야 대회 개별참여 수상",
+ send: "MAJOR-AWARD_CAREER-OUT_SCHOOL-UNOFFICIAL",
},
+ {
+ name: "연합 해커톤 수상",
+ send: "MAJOR-AWARD_CAREER-OUT_SCHOOL-HACKATHON",
+ },
+ {
+ name: "GSM FESTIVAL 수상",
+ send: "MAJOR-AWARD_CAREER-IN_SCHOOL-GSMFEST",
+ },
+ {
+ name: "교내해커톤 수상",
+ send: "MAJOR-AWARD_CAREER-IN_SCHOOL-SCHOOL_HACKATHON",
+ },
+ {
+ name: "전공동아리 발표 수상",
+ send: "MAJOR-AWARD_CAREER-IN_SCHOOL-PRESENTATION",
+ },
+ {
+ name: "전공(심화), 취업 동아리 참여 - 1학기",
+ send: "MAJOR-CLUB_ATTENDANCE_SEMESTER_1",
+ },
+ {
+ name: "전공(심화), 취업 동아리 참여 - 2학기",
+ send: "MAJOR-CLUB_ATTENDANCE_SEMESTER_2",
+ },
+ {
+ name: "공문을 통한 전공분야 대회 참가",
+ send: "MAJOR-OUT_SCHOOL-ATTENDANCE_OFFICIAL_CONTEST",
+ },
+ {
+ name: "전공분야 대회 개별참여 참가",
+ send: "MAJOR-OUT_SCHOOL-ATTENDANCE_UNOFFICIAL_CONTEST",
+ },
+ {
+ name: "연합 해커톤 참가",
+ send: "MAJOR-OUT_SCHOOL-ATTENDANCE_HACKATHON",
+ },
+ {
+ name: "전공 관련 교육 프로그램(세미나 등) 참가",
+ send: "MAJOR-OUT_SCHOOL-ATTENDANCE_SEMINAR",
+ },
+ {
+ name: "GSM FESTIVAL 참가",
+ send: "MAJOR-IN_SCHOOL-ATTENDANCE_GSMFEST",
+ },
+ {
+ name: "교내 해커톤 참가",
+ send: "MAJOR-IN_SCHOOL-ATTENDANCE_HACKATHON",
+ },
+ {
+ name: "전공동아리 발표대회 참가",
+ send: "MAJOR-IN_SCHOOL-ATTENDANCE_CLUB-PRESENTATION",
+ },
+ {
+ name: "전공특강(방과후) 참가",
+ send: "MAJOR-IN_SCHOOL-ATTENDANCE_SEMINAR",
+ },
+ {
+ name: "전공 관련 방과후학교 이수",
+ send: "MAJOR-IN_SCHOOL-ATTENDANCE_AFTER-SCHOOL",
+ },
+];
+
+export const HumanitiesOptions = [
{
name: "자기주도적 활동",
send: "HUMANITIES-ACTIVITIES-SELF-DIRECTED_ACTIVITIES",
},
];
+
+export const ForeignOptions = [
+ {
+ name: "TOEIC",
+ send: "FOREIGN_LANG-TOEIC_SCORE",
+ },
+ {
+ name: "TOEFL",
+ send: "FOREIGN_LANG-TOEFL_SCORE",
+ },
+ {
+ name: "TEPS",
+ send: "FOREIGN_LANG-TEPS_SCORE",
+ },
+ {
+ name: "TOEIC SPEAKING",
+ send: "FOREIGN_LANG-TOEIC_SPEAKING_LEVEL",
+ },
+ {
+ name: "OPIC",
+ send: "FOREIGN_LANG-OPIC_SCORE",
+ },
+ {
+ name: "JPT",
+ send: "FOREIGN_LANG-JPT_SCORE",
+ },
+ {
+ name: "CPT",
+ send: "FOREIGN_LANG-CPT_SCORE",
+ },
+ {
+ name: "HSK",
+ send: "FOREIGN_LANG-HSK_SCORE",
+ },
+];
diff --git a/apps/client/src/widgets/write/model/foreignOptions.ts b/apps/client/src/widgets/write/model/foreignOptions.ts
deleted file mode 100644
index c03a0ebb..00000000
--- a/apps/client/src/widgets/write/model/foreignOptions.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export const foreignOptions = [
- { name: "TOEIC", send: "FOREIGN_LANG-TOEIC_SCORE" },
- { name: "TOEFL", send: "FOREIGN_LANG-TOEFL_SCORE" },
- { name: "TEPS", send: "FOREIGN_LANG-TEPS_SCORE" },
- {
- name: "TOEIC SPEAKING",
- send: "FOREIGN_LANG-TOEIC_SPEAKING_LEVEL",
- },
- { name: "OPIC", send: "FOREIGN_LANG-OPIC_SCORE" },
- { name: "JPT", send: "FOREIGN_LANG-JPT_SCORE" },
- { name: "CPT", send: "FOREIGN_LANG-CPT_SCORE" },
- { name: "HSK", send: "FOREIGN_LANG-HSK_SCORE" },
-];
diff --git a/apps/client/src/widgets/write/model/writeConfig.ts b/apps/client/src/widgets/write/model/writeConfig.ts
index 21eb5fad..8331a54c 100644
--- a/apps/client/src/widgets/write/model/writeConfig.ts
+++ b/apps/client/src/widgets/write/model/writeConfig.ts
@@ -1,27 +1,32 @@
-import { sendScore } from "@/shared/api/sendScore";
-import { majorCategoryOptions } from "@/widgets/calculate/model/category";
-import type { FormValues } from "@/widgets/edit/types/types";
-import { CharacterCategory } from "@/widgets/write/model/category";
+import type { AxiosResponse } from "axios";
-import { handleSubmitBook } from "../lib/handleBookSubmit";
-import { handleSubmitActivity } from "../lib/handleSubmitActivity";
+import type { ConfigType } from "@/shared/model/config";
+import type { FormValues, Option } from "@/shared/model/formValues";
+import {
+ MajorOptions,
+ HumanitiesOptions,
+ ForeignOptions,
+} from "@/widgets/write/model/category";
-import { foreignOptions } from "./foreignOptions";
+import { postScoring } from "../api/postScoring";
+import { handleSubmitActivity } from "../lib/handleSubmitActivity";
+import { handleSubmitReading } from "../lib/handleSubmitReading";
interface Config {
title: string;
- categoryOptions?: { name: string; send: string }[];
- onSubmit: (data: FormValues, type: "draft" | "submit") => Promise;
+ categoryOptions?: Option[];
+ onSubmit: (
+ data: FormValues,
+ type: "draft" | "submit"
+ ) => Promise;
}
-export const getWriteConfig = (
- type: "major" | "humanities" | "reading" | "others" | "foreign"
-): Config => {
+export const getWriteConfig = (type: ConfigType): Config => {
switch (type) {
case "major": {
return {
title: "전공 영역",
- categoryOptions: majorCategoryOptions,
+ categoryOptions: MajorOptions,
onSubmit: async (data: FormValues, type) => {
const formData = new FormData();
if (data.file) {
@@ -35,14 +40,14 @@ export const getWriteConfig = (
formData.append("content", data.content || "");
formData.append("activityType", "MAJOR");
- await handleSubmitActivity(type, formData);
+ return await handleSubmitActivity(type, formData);
},
};
}
case "humanities": {
return {
title: "인성 영역",
- categoryOptions: CharacterCategory,
+ categoryOptions: HumanitiesOptions,
onSubmit: async (data: FormValues, type) => {
const formData = new FormData();
if (data.file) {
@@ -56,7 +61,7 @@ export const getWriteConfig = (
formData.append("content", data.content || "");
formData.append("activityType", "HUMANITIES");
- await handleSubmitActivity(type, formData);
+ return await handleSubmitActivity(type, formData);
},
};
}
@@ -71,30 +76,22 @@ export const getWriteConfig = (
content: data.content || "",
draftId: data.draftId ?? null,
};
- await handleSubmitBook(bookData, type);
+ return await handleSubmitReading(bookData, type);
},
};
}
case "others": {
- return {
- title: "기타 증빙 자료",
- onSubmit: async () => {
- // No submission logic implemented for 'others' category yet.
- },
- };
- }
- case "foreign": {
return {
title: "외국어 영역",
- categoryOptions: foreignOptions,
+ categoryOptions: ForeignOptions,
onSubmit: async (data: FormValues) => {
const formData = new FormData();
if (data.file) {
formData.append("file", data.file);
}
formData.append("categoryName", data.categoryName?.send ?? "");
- formData.append("value", data.title || "");
- await sendScore(formData);
+ formData.append("value", String(data.value));
+ return await postScoring(formData);
},
};
}
diff --git a/apps/client/src/widgets/write/ui/index.tsx b/apps/client/src/widgets/write/ui/index.tsx
index c90009bc..45f69804 100644
--- a/apps/client/src/widgets/write/ui/index.tsx
+++ b/apps/client/src/widgets/write/ui/index.tsx
@@ -3,35 +3,53 @@
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 { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useRef } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
+import { toast } from "sonner";
+import type { ConfigType } from "@/shared/model/config";
+import type { HttpError } from "@/shared/model/error";
+import type { FormValues } from "@/shared/model/formValues";
import { Dropdown, File, Textarea } from "@/shared/ui";
import type { Option } from "@/shared/ui/dropdown";
-import type { FormValues } from "@/widgets/edit/types/types";
import { chooseDropdownOption } from "@/widgets/write/lib/chooseDropdownOption";
import { getWriteConfig } from "../model/writeConfig";
export default function WriteForm() {
- const searchParams = useSearchParams();
const router = useRouter();
- const type = searchParams.get("type") as
- | "major"
- | "humanities"
- | "reading"
- | "others"
- | "foreign";
+ const searchParams = useSearchParams();
+ const type = searchParams.get("type") as ConfigType
const config = getWriteConfig(type);
const submitTypeRef = useRef<"draft" | "submit">("submit");
+ const { mutate } = useMutation({
+ mutationFn: (data: FormValues) => config.onSubmit(data, submitTypeRef.current),
+ onSuccess: (data) => {
+ if (data.status === 201) {
+ toast.success("증빙자료를 성공적으로 저장하였습니다.")
+ router.push("/");
+ }
+ },
+ onError: (error: HttpError) => {
+ if (error.httpStatus === HttpStatusCode.NotFound) {
+ toast.error("해당 증빙 자료가 존재하지 않습니다.");
+ } else if (error.httpStatus === HttpStatusCode.UnprocessableEntity) {
+ toast.error("더 이상 증빙 자료를 추가할 수 없습니다.");
+ } else {
+ toast.error("증빙 자료 추가에 실패했습니다.");
+ }
+ }
+ })
+
const {
handleSubmit,
control,
- getValues,
- formState: { isValid },
+ formState: { isValid, errors },
} = useForm({
mode: "onChange",
defaultValues: {
@@ -42,41 +60,35 @@ export default function WriteForm() {
send: "",
},
file: undefined,
+ author: "",
+ page: 0,
+ value: 0,
},
});
+
const file = useWatch({ control, name: "file" });
- const category = useWatch({
- control,
- name: "categoryName",
- });
+
+ const category = useWatch({ control, name: "categoryName", });
const needDropdown =
category?.name === "OPIC" ||
category?.name === "TOEIC SPEAKING" ||
category?.name === "HSK";
- const handleFormSubmit = useCallback(
- async (data: FormValues) => {
- await config.onSubmit(data, submitTypeRef.current);
- router.push("/");
- },
- [config, router],
- );
+ const handleFormSubmit = useCallback((data: FormValues) => {
+ mutate(data)
+ }, [mutate]);
- const handleWriteSubmit = useCallback(
- (e: React.FormEvent) => {
- e.preventDefault();
- void handleSubmit(handleFormSubmit)();
- },
- [handleFormSubmit, handleSubmit],
- );
+ const handleWriteSubmit = useCallback((e: React.FormEvent) => {
+ e.preventDefault();
+ void handleSubmit(handleFormSubmit)();
+ }, [handleFormSubmit, handleSubmit]);
- const handleTemporarySave = useCallback(() => {
+ const handleSetDraftStatus = useCallback(() => {
submitTypeRef.current = "draft";
- void handleFormSubmit(getValues());
- }, [getValues, handleFormSubmit]);
+ }, []);
- const handleSubmissionSubmit = useCallback(() => {
+ const handleSetWriteStatus = useCallback(() => {
submitTypeRef.current = "submit";
}, []);
@@ -90,8 +102,7 @@ export default function WriteForm() {
className="flex sm:gap-[2rem] gap-[1.5rem] flex-col"
onSubmit={handleWriteSubmit}
>
- {(type === "major" || type === "humanities" || type === "foreign") &&
- config.categoryOptions ? (
+ {type !== "reading" && config.categoryOptions ? (
control={control}
name="categoryName"
@@ -108,7 +119,7 @@ export default function WriteForm() {
rules={{ required: "카테고리를 선택해주세요." }}
/>
) : null}
- {type === "foreign" && needDropdown ? (
+ {type === "others" && needDropdown ? (
control={control}
name="value"
@@ -125,48 +136,60 @@ export default function WriteForm() {
rules={{ required: "카테고리를 선택해주세요." }}
/>
) : null}
-
- {type !== "foreign" && (
-
+ {type !== "others" && (
+
control={control}
name="title"
rules={{
+ maxLength: {
+ value: 20,
+ message: "20자 이내로 입력해주세요"
+ },
required: "제목을 입력해주세요.",
}}
/>
- )}
- {(type === "reading" || (type === "foreign" && !needDropdown)) && (
+ )}
+ {(type === "reading" || (type === "others" && !needDropdown)) && (
<>
{type === "reading" && (
-
+
control={control}
name="author"
rules={{
+ maxLength: {
+ value: 20,
+ message: "20자 이내로 입력해주세요"
+ },
required: "저자를 입력해주세요.",
}}
/>
)}
-
+
control={control}
name={type === "reading" ? "page" : "value"}
rules={{
+ valueAsNumber: true,
+ max: {
+ value: 10_000,
+ message: "최대 10000까지만 입력가능합니다."
+ },
required:
type === "reading"
? "페이지를 입력해주세요."
: "점수를 입력해주세요.",
}}
+ type="number"
/>
>
)}
-
- {type !== "foreign" && (
+ {type !== "others" && (
control={control}
name="content"
@@ -193,39 +216,36 @@ export default function WriteForm() {
}}
/>
)}
-
- {(type === "major" ||
- type === "humanities" ||
- type === "foreign") && (
-
- control={control}
- name="file"
- // eslint-disable-next-line react/jsx-no-bind
- render={({ field: { value, onChange, ...field } }) => (
-
- )}
- rules={{
- ...(type === "foreign" && {
- required: "파일을 첨부해주세요.",
- }),
- }}
- />
- )}
-
+ {(type !== "reading") && (
+
+ control={control}
+ name="file"
+ // eslint-disable-next-line react/jsx-no-bind
+ render={({ field: { value, onChange, ...field } }) => (
+
+ )}
+ rules={{
+ ...(type === "others" && {
+ required: "파일을 첨부해주세요.",
+ }),
+ }}
+ />
+ )}
- {type !== "foreign" && (
+ {type !== "others" && (
)}
diff --git a/packages/api/src/axios.ts b/packages/api/src/axios.ts
index e9db9121..e9ee4138 100644
--- a/packages/api/src/axios.ts
+++ b/packages/api/src/axios.ts
@@ -6,6 +6,7 @@ import axios, {
type InternalAxiosRequestConfig,
type CreateAxiosDefaults,
AxiosHeaders,
+ AxiosError,
} from "axios";
const TIMEOUT = 10_000;
@@ -40,7 +41,7 @@ if (typeof globalThis.window === "object") {
instance.interceptors.response.use(
(response) => response,
- (error) => {
+ (error: AxiosError) => {
if (error.response?.status === 403) {
deleteCookie("accessToken");
deleteCookie("refreshToken");
diff --git a/packages/shared/src/button.tsx b/packages/shared/src/button.tsx
index dbb6557b..22bd7ffb 100644
--- a/packages/shared/src/button.tsx
+++ b/packages/shared/src/button.tsx
@@ -20,7 +20,10 @@ const Button = ({
...props
}: ButtonProps & ButtonHTMLAttributes) => {
const baseStyle =
- "flex cursor-pointer py-2 sm:py-[0.8125rem] w-full text-body2 px-3 sm:px-4 md:px-5 lg:px-[1.375rem] justify-center items-center self-stretch rounded-[0.75rem] transition-colors whitespace-nowrap";
+ `w-full text-body2 rounded-[0.75rem] transition-colors whitespace-nowrap
+ flex justify-center items-center self-stretch ${state == "disabled" ? "" : "cursor-pointer"}
+ py-2 px-3 sm:py-[0.8125rem] sm:px-4 md:px-5 lg:px-[1.375rem]`;
+
const buttonStyle = useMemo(
() => variantStyles[variant][state],
[variant, state],
diff --git a/packages/shared/src/card.tsx b/packages/shared/src/card.tsx
index 606b74db..d68234ff 100644
--- a/packages/shared/src/card.tsx
+++ b/packages/shared/src/card.tsx
@@ -48,7 +48,7 @@ const Card = ({
className={`flex justify-between w-[100%] cursor-pointer h-[69px] text-gray-600 text-label py-[1.5rem] px-[2rem] ${className}`}
>
- {front}
+ {front}
{Pending === true && * }
diff --git a/packages/shared/src/confirmModal.tsx b/packages/shared/src/confirmModal.tsx
index fbe99722..e48cb78e 100644
--- a/packages/shared/src/confirmModal.tsx
+++ b/packages/shared/src/confirmModal.tsx
@@ -9,12 +9,8 @@ interface ConfirmModalProps {
const ConfirmModal = ({ description, title, confirm, cancel }: ConfirmModalProps) => {
return (
-
-
+
+
{title}
diff --git a/packages/shared/src/countUp.tsx b/packages/shared/src/countUp.tsx
new file mode 100644
index 00000000..cbd788a4
--- /dev/null
+++ b/packages/shared/src/countUp.tsx
@@ -0,0 +1,75 @@
+import { useEffect, useRef, useState } from "react";
+
+interface CountUpProps {
+ to: number;
+ from?: number;
+ delay?: number;
+ duration?: number;
+ className?: string;
+ separator?: string;
+ onStart?: () => void;
+ onEnd?: () => void;
+}
+
+const easeOutQuart = (t: number) => 1 - Math.pow(1 - t, 4);
+
+const CountUp = ({
+ to,
+ from = 0,
+ delay = 0,
+ duration = 2000,
+ className = "",
+ separator = "",
+ onStart,
+ onEnd,
+}: CountUpProps) => {
+ const [count, setCount] = useState(from);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const startAnimation = () => {
+ if (onStart) onStart();
+
+ const startTime = Date.now();
+ const startValue = from;
+ const endValue = to;
+ const change = endValue - startValue;
+
+ const animate = () => {
+ const elapsed = Date.now() - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ const easedProgress = easeOutQuart(progress);
+ const currentValue = Math.round(startValue + change * easedProgress);
+
+ setCount(currentValue);
+
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ } else {
+ if (onEnd) onEnd();
+ }
+ };
+
+ requestAnimationFrame(animate);
+ };
+
+ const timeoutId = setTimeout(startAnimation, delay);
+ return () => { clearTimeout(timeoutId); };
+ }, [from, to, duration, delay, onStart, onEnd]);
+
+ const formatNumber = (num: number) => {
+ if (separator) {
+ return num.toLocaleString('ko-KR');
+ }
+ return num.toString();
+ };
+
+ return (
+
+ {formatNumber(count)}점
+
+ );
+}
+
+export default CountUp;
\ No newline at end of file
diff --git a/packages/shared/src/eyeClose.tsx b/packages/shared/src/eyeClose.tsx
new file mode 100644
index 00000000..b5464db7
--- /dev/null
+++ b/packages/shared/src/eyeClose.tsx
@@ -0,0 +1,6 @@
+export const EyeClose = () => {
+ return (
+
+
+ )
+}
diff --git a/packages/shared/src/eyeOpen.tsx b/packages/shared/src/eyeOpen.tsx
new file mode 100644
index 00000000..8c93256a
--- /dev/null
+++ b/packages/shared/src/eyeOpen.tsx
@@ -0,0 +1,8 @@
+
+export const EyeOpen = () => {
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/packages/shared/src/input.tsx b/packages/shared/src/input.tsx
index b2afbc45..08ed1e24 100644
--- a/packages/shared/src/input.tsx
+++ b/packages/shared/src/input.tsx
@@ -1,196 +1,35 @@
-"use client";
-import { useState, useEffect, useRef } from "react";
-import { useController } from "react-hook-form";
-import type { UseControllerProps, FieldValues } from "react-hook-form";
-
-interface InputProps
- extends UseControllerProps {
- onChange?: (e: React.ChangeEvent) => void;
+import type { HTMLInputTypeAttribute } from "react"
+import { useController, type Control, type FieldValues, type Path, type RegisterOptions } from "react-hook-form";
+
+interface InputProps {
+ name: Path;
+ control: Control;
+ rules?: RegisterOptions>;
+ type?: HTMLInputTypeAttribute;
className?: string;
- type?: string;
- isEmail?: boolean;
- min?: number;
- max?: number;
}
-const SUFFIX = "@gsm.hs.kr";
-
-const isValidEmailPrefix = (value: string): boolean => {
- if (value.length > 6) return false;
- if (value.length > 0 && !/^[0-9s]/.test(value)) return false;
- if (value.length > 1 && !/^[0-9s][0-9]*$/.test(value)) return false;
- return true;
-};
-
-const Input = ({
- type,
- className,
- isEmail = false,
- min,
- max,
- ...props
-}: InputProps) => {
- const { field } = useController(props);
- const [displayValue, setDisplayValue] = useState("");
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (isEmail && typeof field.value === "string") {
- const value = field.value as string;
- const valueWithoutSuffix = value.endsWith(SUFFIX)
- ? value.slice(0, -SUFFIX.length)
- : value;
- setDisplayValue(valueWithoutSuffix);
- } else {
- const value = typeof field.value === "string" ? field.value : "";
- setDisplayValue(value);
- }
- }, [field.value, isEmail]);
-
- const handleChange = (e: React.ChangeEvent) => {
- const newValue = e.target.value;
-
- if (isEmail) {
- if (newValue.endsWith(SUFFIX)) {
- const prefix = newValue.slice(0, newValue.length - SUFFIX.length);
- if (isValidEmailPrefix(prefix)) {
- setDisplayValue(prefix);
- field.onChange(prefix + SUFFIX);
- }
- } else {
- const cursorPosition = e.target.selectionStart ?? 0;
- const currentValueLength = displayValue.length;
-
- if (cursorPosition <= currentValueLength) {
- const newPrefix =
- newValue.length > currentValueLength + SUFFIX.length
- ? newValue.slice(0, -SUFFIX.length)
- : newValue;
-
- if (isValidEmailPrefix(newPrefix)) {
- setDisplayValue(newPrefix);
- field.onChange(newPrefix + SUFFIX);
- }
- } else {
- e.target.value = displayValue + SUFFIX;
- }
- }
- } else {
- setDisplayValue(newValue);
- field.onChange(newValue);
- }
- };
-
- const handleSelect = (e: React.SyntheticEvent) => {
- if (isEmail && inputRef.current) {
- const input = e.target as HTMLInputElement;
- const prefixLength = displayValue.length;
- const selectionEnd = input.selectionEnd ?? 0;
-
- if (selectionEnd > prefixLength) {
- const selectionStart = input.selectionStart ?? 0;
- input.setSelectionRange(selectionStart, prefixLength);
- }
- }
- };
-
- const handleClick = (e: React.MouseEvent) => {
- if (isEmail && inputRef.current) {
- const input = e.target as HTMLInputElement;
- const prefixLength = displayValue.length;
- const selectionStart = input.selectionStart ?? 0;
-
- if (selectionStart > prefixLength) {
- input.setSelectionRange(prefixLength, prefixLength);
- }
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (isEmail && inputRef.current) {
- const input = e.target as HTMLInputElement;
- const prefixLength = displayValue.length;
- const cursorPosition = input.selectionStart ?? 0;
-
- if (e.key === "a" && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- const fullLength = (displayValue + SUFFIX).length;
- input.setSelectionRange(0, fullLength);
- return;
- }
-
- if (cursorPosition > prefixLength) {
- const allowedKeys = [
- "ArrowLeft",
- "ArrowRight",
- "ArrowUp",
- "ArrowDown",
- "Home",
- "End",
- "Tab",
- ];
- if (!allowedKeys.includes(e.key)) {
- e.preventDefault();
- }
- return;
- }
-
- const alwaysAllowedKeys = [
- "Backspace",
- "Delete",
- "ArrowLeft",
- "ArrowRight",
- "ArrowUp",
- "ArrowDown",
- "Home",
- "End",
- "Tab",
- ];
- if (alwaysAllowedKeys.includes(e.key)) {
- return;
- }
-
- if (cursorPosition === 0) {
- if (!/^[0-9s]$/.test(e.key)) {
- e.preventDefault();
- }
- } else {
- if (!/^[0-9]$/.test(e.key)) {
- e.preventDefault();
- }
- }
-
- const selectionStart = input.selectionStart ?? 0;
- const selectionEnd = input.selectionEnd ?? 0;
- const hasSelection = selectionStart !== selectionEnd;
-
- if (
- displayValue.length >= 6 &&
- !e.ctrlKey &&
- !e.metaKey &&
- !hasSelection
- ) {
- e.preventDefault();
- }
- }
- };
+const Input = ({ name, control, rules, type = "text", className }: InputProps) => {
+ const { field: { onChange, onBlur, value, ref }, fieldState: { error } } = useController({ name, control, rules })
return (
-
- );
-};
+
+ )
+}
-export { Input };
+export { Input }
\ No newline at end of file
diff --git a/packages/shared/src/inputContainer.tsx b/packages/shared/src/inputContainer.tsx
index 329a1aa8..14692243 100644
--- a/packages/shared/src/inputContainer.tsx
+++ b/packages/shared/src/inputContainer.tsx
@@ -1,17 +1,22 @@
-// const React = require("react") as typeof import("react");
+import type { FieldError } from "react-hook-form";
interface InputContainerProps {
label: string;
- children: React.ReactNode;
+ htmlFor: string;
+ children?: React.ReactNode;
+ error: FieldError | undefined;
}
-const InputContainer = ({ label, children }: InputContainerProps) => {
+const InputContainer = ({ label, htmlFor, children, error }: InputContainerProps) => {
return (
-
- {label}
+
- );
-};
+
+ {error?.message ?? "\u00A0"}
+
+
+ )
+}
-export { InputContainer };
+export { InputContainer }
\ No newline at end of file
diff --git a/packages/shared/src/list.tsx b/packages/shared/src/list.tsx
index ef8e9788..3537ece3 100644
--- a/packages/shared/src/list.tsx
+++ b/packages/shared/src/list.tsx
@@ -6,7 +6,7 @@ import { Filtered } from "./filtered.tsx";
interface ListProps {
children: React.ReactNode;
- title: string;
+ title?: string;
className?: string;
isFilter?: boolean;
onClick?: () => void;
@@ -20,20 +20,23 @@ const List = ({
onClick,
}: ListProps) => {
return (
-
+
-
+
{title}
{isFilter ? (
-
+
) : null}
-
{children}
+
+ {children}
+
);
};
diff --git a/packages/store/src/postProvider.tsx b/packages/store/src/postProvider.tsx
index ab331265..e1ecdaa5 100644
--- a/packages/store/src/postProvider.tsx
+++ b/packages/store/src/postProvider.tsx
@@ -1,24 +1,24 @@
"use client";
-import type { Draft } from "@repo/types/draft";
-import type { post } from "@repo/types/evidences";
+import type { DraftType } from "@repo/types/draft";
+import type { PostType } from "@repo/types/evidences";
import { createContext, useContext, useState } from "react";
import type { ReactNode } from "react";
interface PostContextType {
- post: post | Draft | null;
- setPost: (post: post | Draft) => void;
+ post: PostType | DraftType | undefined;
+ setPost: (post: PostType | DraftType) => void;
}
const postContext = createContext
({
- post: null,
+ post: undefined,
setPost: () => {
throw new Error("PostContext not initialized");
},
});
const PostProvider = ({ children }: { children: ReactNode }) => {
- const [post, setPost] = useState(null);
+ const [post, setPost] = useState();
return (
diff --git a/packages/types/src/draft.ts b/packages/types/src/draft.ts
index 44314c4f..e0646a79 100644
--- a/packages/types/src/draft.ts
+++ b/packages/types/src/draft.ts
@@ -1,11 +1,25 @@
-import type { Activity, Reading } from "@repo/types/evidences";
+export type ActivityType = "MAJOR" | "HUMANITIES";
-export interface DraftResponse {
- activityEvidences: Draft[];
- readingEvidences: Draft[];
+export interface ActivityDraft {
+ draftId: string;
+ title: string;
+ content: string;
+ imageUri?: string;
+ categoryName: string;
+ activityType: ActivityType;
+}
+
+export interface ReadingDraft {
+ draftId: string;
+ title: string;
+ content: string;
+ author: string;
+ page: number;
}
-type ActivityDraft = Omit & { draftId: string };
-type ReadingDraft = Omit & { draftId: string };
+export type DraftType = ActivityDraft | ReadingDraft;
-export type Draft = ActivityDraft | ReadingDraft;
+export interface DraftResponse {
+ activityEvidences: ActivityDraft[];
+ readingEvidences: ReadingDraft[];
+}
diff --git a/packages/types/src/evidences.ts b/packages/types/src/evidences.ts
index 58ed03a7..cac9a850 100644
--- a/packages/types/src/evidences.ts
+++ b/packages/types/src/evidences.ts
@@ -1,51 +1,51 @@
+export type EvidenceType =
+ | "MAJOR"
+ | "HUMANITIES"
+ | "READING"
+ | "FOREIGN_LANGUAGE"
+ | "CERTIFICATE"
+ | "TOPCIT"
+ | "READ-A-THON"
+ | "TOEIC"
+ | "TOEFL"
+ | "TEPS"
+ | "TOEIC_SPEAKING"
+ | "OPIC"
+ | "JPT"
+ | "CPT"
+ | "HSK";
+
+export type PostStatus = "PENDING" | "APPROVE" | "REJECT";
+
export interface Activity {
id: number;
title: string;
content: string;
imageUri?: string;
- status: postState;
categoryName: string;
+ status: PostStatus;
}
export interface Reading {
id: number;
title: string;
+ content: string;
author: string;
page: number;
- content: string;
- status: postState;
+ status: PostStatus;
}
export interface Others {
id: number;
- fileUri?: string;
evidenceType: EvidenceType;
- status: postState;
+ fileUri: string;
categoryName: string;
+ status: PostStatus;
}
-export type postState = "APPROVE" | "PENDING" | "REJECT";
-
-export type post = Activity | Reading | Others;
-
-export type EvidenceType =
- | "MAJOR"
- | "HUMANITIES"
- | "READING"
- | "FOREIGN_LANGUAGE"
- | "CERTIFICATE"
- | "TOPCIT"
- | "READ-A-THON"
- | "TOEIC"
- | "TOEFL"
- | "TEPS"
- | "TOEIC_SPEAKING"
- | "OPIC"
- | "JPT"
- | "CPT"
- | "HSK";
+export type PostType = Activity | Reading | Others;
-export interface EvidenceResponse {
+export interface PostResponse {
majorActivityEvidence: Activity[];
humanitiesActivityEvidence: Activity[];
readingEvidence: Reading[];
diff --git a/packages/types/src/member.ts b/packages/types/src/member.ts
index e7b033f9..9fabe517 100644
--- a/packages/types/src/member.ts
+++ b/packages/types/src/member.ts
@@ -9,8 +9,4 @@ export interface Member {
hasPendingEvidence?: boolean;
}
-export type role =
- | "ROLE_STUDENT"
- | "ROLE_HOOMROOM_TEACHER"
- | "ROLE_MAISTER_TEACHER"
- | "ROLE_DEVELOPER";
+export type role = "ROLE_ADMIN" | "ROLE_STUDENT" | "ROLE_TEACHER";
diff --git a/packages/utils/src/handleCategory.ts b/packages/utils/src/handleCategory.ts
index 367b418c..4375e6a6 100644
--- a/packages/utils/src/handleCategory.ts
+++ b/packages/utils/src/handleCategory.ts
@@ -67,7 +67,7 @@ export const categoryMapping: Record = {
"FOREIGN_LANG-TEPS_SCORE": "외국어 영역-외국어 공인 시험-TEPS",
"FOREIGN_LANG-TOEIC_SPEAKING_LEVEL":
"외국어 영역-외국어 공인 시험-TOEIC Speaking",
- "FOREIGN_LANG-OPIC_SCORE": "외국어 영역-외국어 공인 시험-OPIc",
+ "FOREIGN_LANG-OPIC_SCORE": "외국어 영역-외국어 공인 시험-OPIC",
"FOREIGN_LANG-JPT_SCORE": "외국어 영역-외국어 공인 시험-JPT",
"FOREIGN_LANG-CPT_SCORE": "외국어 영역-외국어 공인 시험-CPT",
"FOREIGN_LANG-HSK_SCORE": "외국어 영역-외국어 공인 시험-HSK",
diff --git a/packages/utils/src/handlePost.ts b/packages/utils/src/handlePost.ts
index 2e399ae8..cea8f423 100644
--- a/packages/utils/src/handlePost.ts
+++ b/packages/utils/src/handlePost.ts
@@ -1,15 +1,31 @@
-import type { Activity, Others, Reading } from "@repo/types/evidences";
+import type { DraftType, ActivityDraft, ReadingDraft } from "@repo/types/draft";
+import type {
+ Activity,
+ Reading,
+ Others,
+ PostType,
+} from "@repo/types/evidences";
-const isActivity = (data: Activity | Reading | Others): data is Activity => {
+function isDraft(data: PostType | DraftType): data is DraftType {
+ return typeof data === "object" && "draftId" in data;
+}
+
+function isPost(data: PostType | DraftType): data is PostType {
+ return typeof data === "object" && "id" in data;
+}
+
+function isActivity(
+ data: PostType | DraftType
+): data is Activity | ActivityDraft {
return "imageUri" in data;
-};
+}
-const isReading = (data: Activity | Reading | Others): data is Reading => {
+function isReading(data: PostType | DraftType): data is Reading | ReadingDraft {
return "author" in data;
-};
+}
-const isOthers = (data: Activity | Reading | Others): data is Others => {
+function isOthers(data: PostType | DraftType): data is Others {
return !isActivity(data) && !isReading(data);
-};
+}
-export { isActivity, isReading, isOthers };
+export { isDraft, isPost, isActivity, isReading, isOthers };