diff --git a/app/(afterLogin)/mypage/MypageActivity.tsx b/app/(afterLogin)/mypage/MypageActivity.tsx
new file mode 100644
index 0000000..38bd315
--- /dev/null
+++ b/app/(afterLogin)/mypage/MypageActivity.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+
+import {
+ LikedArticleCard,
+ MyArticleCard,
+} from "@/components/mypage/articleCard";
+import { PaginationButton } from "@/components/pagination-button/pagination-button";
+import { toasts } from "@/components/shared/toast";
+import { useLikedPrompts, useUserPrompts } from "@/hooks/use-mypage";
+import { cn } from "@/lib/utils";
+
+export default function MypageActivitySection() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ // 1. 현재 탭 및 페이지 상태 관리
+ const rawSub = searchParams.get("sub");
+ const activeSub: "liked" | "posted" =
+ rawSub === "posted" ? "posted" : "liked";
+ const currentPage = Number(searchParams.get("page")) || 0;
+ const userId = 1; // 실제로는 인증 정보나 프로필 훅에서 가져온 ID 사용
+
+ const { data: likedData, isLoading: isLikedLoading } = useLikedPrompts(
+ userId,
+ currentPage
+ );
+ const { data: postedData, isLoading: isPostedLoading } = useUserPrompts({
+ userId,
+ page: currentPage,
+ });
+
+ const handleSubTabChange = (sub: "liked" | "posted") => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("sub", sub);
+ params.set("page", "0");
+ router.push(`/mypage?${params.toString()}`, { scroll: false });
+ };
+
+ const handleCopy = async (e: React.MouseEvent, content: string) => {
+ e.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(content);
+ toasts.success("프롬프트가 클립보드에 복사되었습니다!");
+ } catch {
+ alert("복사에 실패했습니다.");
+ }
+ };
+ const handlePageChange = (page: number) => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("page", page.toString());
+ router.push(`/mypage?${params.toString()}`, { scroll: false });
+ };
+
+ const handleCardClick = (id: number) => {
+ router.push(`/prompts/${id}`);
+ };
+
+ // 현재 활성화된 데이터와 로딩 상태 결정
+ const currentData = activeSub === "liked" ? likedData : postedData;
+ const isLoading = activeSub === "liked" ? isLikedLoading : isPostedLoading;
+
+ return (
+
+ {/* 서브 탭 메뉴 */}
+
+
+
+
+
+ {/* 리스트 영역 */}
+
+ {isLoading ? (
+
로딩 중...
+ ) : currentData?.content.length === 0 ? (
+
+ {activeSub === "liked"
+ ? "좋아요한 프롬프트가 없습니다."
+ : "게시한 프롬프트가 없습니다."}
+
+ ) : (
+ currentData?.content.map((prompt) =>
+ activeSub === "liked" ? (
+
handleCopy(e, prompt.description)}
+ onClick={() => handleCardClick(prompt.promptId)}
+ />
+ ) : (
+ handleCardClick(prompt.promptId)}
+ />
+ )
+ )
+ )}
+
+
+ {/* 단순 페이지네이션 (필요 시 추가) */}
+ {!isLoading && (currentData?.pageInfo.totalPages ?? 0) > 1 && (
+
+ )}
+
+ );
+}
diff --git a/app/(afterLogin)/mypage/MypageLeftSection.tsx b/app/(afterLogin)/mypage/MypageLeftSection.tsx
new file mode 100644
index 0000000..b512796
--- /dev/null
+++ b/app/(afterLogin)/mypage/MypageLeftSection.tsx
@@ -0,0 +1,96 @@
+"use client";
+import { useQueryClient } from "@tanstack/react-query";
+import Cookies from "js-cookie";
+import { useRouter } from "next/navigation";
+
+import UserProfile from "@/components/mypage/user-profile";
+import { BaseButton } from "@/components/shared/button";
+import { toasts } from "@/components/shared/toast";
+import { useUserProfile } from "@/hooks/use-mypage";
+import { deleteUserAccount, postLogout } from "@/queries/api/auth";
+
+export default function MypageLeftSection() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+
+ const { data: userData, isLoading } = useUserProfile();
+
+ const handleLogout = async () => {
+ if (confirm("로그아웃 하시겠습니까?")) {
+ try {
+ await postLogout();
+
+ Cookies.remove("accessToken");
+ Cookies.remove("refreshToken");
+
+ queryClient.clear();
+ toasts.success("로그아웃 되었습니다.");
+ router.push("/");
+ router.refresh();
+ } catch (error) {
+ Cookies.remove("accessToken");
+ Cookies.remove("refreshToken");
+ console.error("Logout failed:", error);
+ //TODO: error 컴포넌트가 생기면 사용자 피드백 주기
+ //toasts.error("로그아웃 처리 중 오류가 발생했습니다.");
+ }
+ }
+ };
+
+ const handleWithdraw = async () => {
+ const isConfirmed = confirm(
+ "정말로 탈퇴하시겠습니까?\n탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다."
+ );
+
+ if (!isConfirmed || !userData) return;
+
+ try {
+ await deleteUserAccount(userData.basicInfo.uid);
+
+ Cookies.remove("refreshToken");
+ Cookies.remove("accessToken");
+ queryClient.clear();
+
+ toasts.success("회원 탈퇴가 완료되었습니다. 이용해 주셔서 감사합니다.");
+ router.push("/");
+ router.refresh();
+ } catch (error) {
+ console.error("Withdrawal failed:", error);
+ //toasts.error("탈퇴 처리 중 오류가 발생했습니다. 고객센터에 문의해주세요.");
+ }
+ };
+
+ // 로딩 및 에러 처리
+ if (isLoading) return 로딩 중...
;
+ if (!userData)
+ return 유저 정보가 없습니다.
;
+
+ const { basicInfo } = userData;
+
+ return (
+
+
+
+
+ 로그아웃
+
+
+
+
+ );
+}
diff --git a/app/(afterLogin)/mypage/MypageRightSection.tsx b/app/(afterLogin)/mypage/MypageRightSection.tsx
new file mode 100644
index 0000000..47d7fbe
--- /dev/null
+++ b/app/(afterLogin)/mypage/MypageRightSection.tsx
@@ -0,0 +1,270 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+
+import {
+ EDUCATION_OPTIONS,
+ EducationValue,
+ JobType,
+ STATE_VALUES,
+ transformCareerInfoToState,
+ transformStateToPayload,
+} from "@/app/(beforeLogin)/(auth)/constant";
+import { StatusSelect } from "@/app/(beforeLogin)/(auth)/signup/component/StatusSelect";
+import { TargetJobsSelect } from "@/app/(beforeLogin)/(auth)/signup/component/TargetJobsSelect";
+import { useMypageStore } from "@/app/store/mypageStore";
+import UserExperience from "@/components/mypage/user-experience";
+import UserKeyword from "@/components/mypage/user-keyword";
+import { BaseButton } from "@/components/shared/button";
+import { BaseCheckBox } from "@/components/shared/checkbox";
+import { BaseInput } from "@/components/shared/inputs";
+import { SelectBox } from "@/components/shared/select-box";
+import { useUpdateProfile, useUserProfile } from "@/hooks/use-mypage";
+import { UpdateProfileRequest } from "@/queries/api/mypage";
+
+export default function MypageRightSection() {
+ const { data: profile, isLoading } = useUserProfile();
+ const { mutate: updateProfile, isPending } = useUpdateProfile();
+
+ const [otherInput, setOtherInput] = useState("");
+ const [experiences, setExperiences] = useState([]);
+ const [keywords, setKeywords] = useState([]);
+ const [marketingAgree, setMarketingAgree] = useState(false);
+
+ const {
+ targetJobs,
+ setTargetJobs,
+ currentState,
+ career,
+ educationLevel,
+ updateField,
+ reset,
+ } = useMypageStore();
+
+ useEffect(() => {
+ return () => reset();
+ }, [reset]);
+
+ //TODO: 약관동의 동의 여부를 조회가능한 API 있는지 확인
+ const initialized = useRef(false);
+ useEffect(() => {
+ if (profile) {
+ if (initialized.current) return;
+ initialized.current = true;
+ const { careerInfo, selfIntro } = profile;
+
+ const normalized = transformCareerInfoToState(careerInfo);
+
+ setTargetJobs(normalized.targetJobs);
+ updateField("currentState", normalized.currentState);
+ updateField("educationLevel", normalized.educationLevel);
+ updateField("career", normalized.career);
+
+ setExperiences(selfIntro.experiences || []);
+ setKeywords(selfIntro.keywords || []);
+ }
+ }, [profile, setTargetJobs, updateField]);
+
+ if (isLoading) {
+ return (
+
+ 정보를 불러오는 중...
+
+ );
+ }
+
+ const handleJobClick = (option: string) => {
+ const castedOption = option as JobType;
+ if (targetJobs.includes(castedOption)) {
+ setTargetJobs(targetJobs.filter((j) => j !== castedOption));
+ } else {
+ setTargetJobs([...targetJobs, castedOption]);
+ }
+ };
+
+ const handleGlobalSave = () => {
+ if (!profile) return;
+
+ if (currentState === STATE_VALUES.OTHER && !otherInput.trim()) {
+ alert("기타 상태를 직접 입력해주세요.");
+ //toasts.error("기타 상태를 직접 입력해주세요.");
+ return;
+ }
+ const careerPayload = transformStateToPayload({
+ currentState,
+ targetJobs,
+ otherInput,
+ career,
+ educationLevel,
+ });
+
+ const payload: UpdateProfileRequest = {
+ basicInfo: {
+ nickname: profile.basicInfo.nickname,
+ introduction: profile.basicInfo.introduction || "",
+ },
+ careerInfo: {
+ ...careerPayload,
+ major: profile.careerInfo.major || "",
+ },
+ selfIntro: {
+ experiences,
+ keywords,
+ },
+ };
+
+ updateProfile(payload);
+ };
+
+ const handleCancel = () => {
+ if (confirm("변경사항이 저장되지 않을 수 있습니다. 취소하시겠습니까?")) {
+ window.location.reload();
+ }
+ };
+
+ return (
+
+ {/* 1. 취업/경력 섹션 */}
+
+ 취업/경력
+ {
+ updateField("currentState", val);
+ if (val !== STATE_VALUES.OTHER) setOtherInput("");
+ }}
+ onOtherChange={setOtherInput}
+ />
+
+
+
+
+
+ updateField("educationLevel", val as EducationValue)
+ }
+ selectOptions={EDUCATION_OPTIONS}
+ />
+
+
+
+ {
+ const val = e.target.value.replace(/[^0-9]/g, "");
+ updateField("career", val === "" ? 0 : Number(val));
+ }}
+ />
+
+
+
+
+ 취소
+
+
+ 변경사항 저장
+
+
+
+
+ {/* 2. 자기소개 프로필 섹션 */}
+
+
+
자기소개 프로필
+
+ 자소서 미리보기에 활용될 정보입니다
+
+
+
+
+
+
+ 취소
+
+
+ 변경사항 저장
+
+
+
+
+ {/* 3. 개인정보 관리 섹션 */}
+
+
+
개인정보 관리
+
+ 현재 적용 중인 개인정보 수집 및 이용 동의 현황입니다.
+
+
+
+ setMarketingAgree(!!checked)}
+ />
+
+ 보기
+
+
+
+
+ 취소
+
+
+ 변경사항 저장
+
+
+
+
+ );
+}
diff --git a/app/(afterLogin)/mypage/MypageTabButtons.tsx b/app/(afterLogin)/mypage/MypageTabButtons.tsx
new file mode 100644
index 0000000..38faeb2
--- /dev/null
+++ b/app/(afterLogin)/mypage/MypageTabButtons.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+
+import { BaseButton } from "@/components/shared/button";
+import { cn } from "@/lib/utils";
+
+export default function MypageTabButtons({ activeTab }: { activeTab: string }) {
+ const router = useRouter();
+
+ const handleTabChange = (tab: "profile" | "activity") => {
+ router.push(`/mypage?tab=${tab}`, { scroll: false });
+ };
+
+ return (
+
+ handleTabChange("profile")}
+ className={cn(
+ "px-4 py-2.5 min-w-[197px] shadow-sm transition-colors",
+ activeTab === "profile"
+ ? "bg-frog-600 text-white"
+ : "bg-gray-0 text-gray-700 border border-gray-100"
+ )}
+ >
+ 프로필 설정
+
+ handleTabChange("activity")}
+ className={cn(
+ "px-4 py-2.5 min-w-[197px] shadow-sm transition-colors",
+ activeTab === "activity"
+ ? "bg-frog-600 text-white"
+ : "bg-gray-0 text-gray-700 border border-gray-100"
+ )}
+ >
+ 내 활동
+
+
+ );
+}
diff --git a/app/(afterLogin)/mypage/layout.tsx b/app/(afterLogin)/mypage/layout.tsx
new file mode 100644
index 0000000..710287f
--- /dev/null
+++ b/app/(afterLogin)/mypage/layout.tsx
@@ -0,0 +1,14 @@
+import { Header } from "@/components/header/header";
+
+export default function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/app/(afterLogin)/mypage/page.tsx b/app/(afterLogin)/mypage/page.tsx
index 06752e9..bf49d90 100644
--- a/app/(afterLogin)/mypage/page.tsx
+++ b/app/(afterLogin)/mypage/page.tsx
@@ -1,15 +1,50 @@
-export default function MyPage() {
- return MyPage
;
+import { PageTitleGroup } from "@/components/shared/page-title-group";
+
+import MypageActivitySection from "./MypageActivity";
+import MypageLeftSection from "./MypageLeftSection";
+import MypageRightSection from "./MypageRightSection";
+import MypageTabButtons from "./MypageTabButtons";
+
+interface Props {
+ searchParams: Promise<{ tab?: string }>;
}
-/**
- * 마이페이지 search params
- *
- * - tab
- * profile
- * tab
- *
- * - sub
- * liked
- * posted
- */
+export default async function MyPage({ searchParams }: Props) {
+ const { tab } = await searchParams;
+ const VALID_TABS = ["profile", "activity"] as const;
+ type ActiveTab = (typeof VALID_TABS)[number];
+ const activeTab: ActiveTab = VALID_TABS.includes(tab as ActiveTab)
+ ? (tab as ActiveTab)
+ : "profile";
+
+ return (
+
+
+
+
+ {/* 탭 버튼 영역 */}
+
+
+
+
+
+
+ {/* 왼쪽: 유저 프로필 */}
+
+
+ {/* 오른쪽: 탭 내용 */}
+
+ {activeTab === "profile" ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/app/(beforeLogin)/(auth)/constant.ts b/app/(beforeLogin)/(auth)/constant.ts
index eda2c7a..9a6515a 100644
--- a/app/(beforeLogin)/(auth)/constant.ts
+++ b/app/(beforeLogin)/(auth)/constant.ts
@@ -5,41 +5,98 @@ export enum AuthProvider {
NAVER = "naver",
}
-export const STATE_OPTIONS = [
- "재학중",
- "취업준비",
- "이직준비",
- "기타",
-] as const;
-export const JOB_OPTIONS = [
- "기획",
- "마케팅",
- "디자인",
- "개발",
- "영업",
- "기타",
-] as const;
-
export const STATE_VALUES = {
- IN_SCHOOL: "재학중",
- JOB_SEEKING: "취업준비",
+ STUDENT: "재학중",
+ JOB_SEEKER: "취업준비",
CHANGING_JOB: "이직준비",
OTHER: "기타",
} as const;
-export type StateType = (typeof STATE_OPTIONS)[number];
-export type JobType = (typeof JOB_OPTIONS)[number];
+export const JOB_DATA = {
+ PLANNING: "기획",
+ MARKETING: "마케팅",
+ DESIGN: "디자인",
+ DEVELOPMENT: "개발",
+ SALES: "영업",
+ PM: "PM",
+ OTHER: "기타",
+} as const;
+
+export const STATE_MAP = new Map(
+ Object.entries(STATE_VALUES) as [string, string][]
+);
+export const JOB_MAP = new Map(
+ Object.entries(JOB_DATA) as [string, string][]
+);
+export const REVERSE_STATE_MAP = new Map(
+ (Object.entries(STATE_VALUES) as [string, string][]).map(([k, v]) => [v, k])
+);
+export const REVERSE_JOB_MAP = new Map(
+ (Object.entries(JOB_DATA) as [string, string][]).map(([k, v]) => [v, k])
+);
+
+export type StateType = (typeof STATE_VALUES)[keyof typeof STATE_VALUES];
+export type StateKey = keyof typeof STATE_VALUES;
+export type JobType = (typeof JOB_DATA)[keyof typeof JOB_DATA];
+export type EducationValue =
+ | "HIGH_SCHOOL"
+ | "ASSOCIATE"
+ | "BACHELOR"
+ | "MASTER"
+ | "DOCTOR";
+
+export const transformCareerInfoToState = (careerInfo: {
+ currentStatus: string[];
+ targetJob: string[];
+ careerYear: string;
+ education: string;
+}) => {
+ const stateInKorean = STATE_MAP.get(careerInfo.currentStatus[0]) || "";
+ const jobsInKorean = careerInfo.targetJob.map(
+ (code) => JOB_MAP.get(code) || code
+ );
+ const parsed = Number(careerInfo.careerYear);
+ const careerNumber =
+ careerInfo.careerYear === "신입" || !Number.isFinite(parsed) ? 0 : parsed;
+ return {
+ currentState: stateInKorean as "" | StateType,
+ targetJobs: jobsInKorean as JobType[],
+ career: careerNumber,
+ educationLevel: careerInfo.education as EducationValue,
+ };
+};
+
+export const transformStateToPayload = (state: {
+ currentState: string;
+ otherInput?: string;
+ targetJobs: string[];
+ career: number;
+ educationLevel: string;
+}) => {
+ const isOther = state.currentState === STATE_VALUES.OTHER;
+ const stateCode = isOther
+ ? state.otherInput || "OTHER"
+ : REVERSE_STATE_MAP.get(state.currentState) || state.currentState;
+
+ const jobCodes = state.targetJobs.map(
+ (label) => REVERSE_JOB_MAP.get(label) || label
+ );
+
+ return {
+ currentStatus: String(stateCode),
+ targetJob: jobCodes,
+ careerYear: state.career,
+ education: state.educationLevel,
+ };
+};
+
+export const STATE_OPTIONS = Object.values(STATE_VALUES);
+export const JOB_OPTIONS = Object.values(JOB_DATA);
export const EDUCATION_OPTIONS: SelectOption[] = [
{ label: "고등학교 졸업", value: "HIGH_SCHOOL" },
{ label: "전문대 졸업", value: "ASSOCIATE" },
- { label: "대학교 졸업", value: "BACHELOR_4" },
+ { label: "대학교 졸업", value: "BACHELOR" },
{ label: "석사 졸업", value: "MASTER" },
{ label: "박사 졸업", value: "DOCTOR" },
];
-
-export const FIELD_OPTIONS: SelectOption[] = [
- { label: "인문계열", value: "HUMANITIES" },
- { label: "이공계열", value: "ENGINEERING_SCIENCE" },
- { label: "예체능계열", value: "ARTS_PHYSICAL" },
-];
diff --git a/app/(beforeLogin)/(auth)/signup/component/Detail.tsx b/app/(beforeLogin)/(auth)/signup/component/Detail.tsx
index a8dc608..06ff7f6 100644
--- a/app/(beforeLogin)/(auth)/signup/component/Detail.tsx
+++ b/app/(beforeLogin)/(auth)/signup/component/Detail.tsx
@@ -86,6 +86,7 @@ export default function Detail() {
/>
{
@@ -95,7 +96,11 @@ export default function Detail() {
onOtherChange={setOtherInput}
/>
-
+
diff --git a/app/(beforeLogin)/(auth)/signup/component/StatusSelect.tsx b/app/(beforeLogin)/(auth)/signup/component/StatusSelect.tsx
index 8b9409b..c68bc04 100644
--- a/app/(beforeLogin)/(auth)/signup/component/StatusSelect.tsx
+++ b/app/(beforeLogin)/(auth)/signup/component/StatusSelect.tsx
@@ -3,6 +3,7 @@ import { BaseInput } from "@/components/shared/inputs";
import { STATE_OPTIONS, STATE_VALUES, StateType } from "../../constant";
interface StatusSelectProps {
+ label: string;
value: StateType | "";
otherValue: string;
onSelect: (status: StateType) => void;
@@ -10,6 +11,7 @@ interface StatusSelectProps {
}
export const StatusSelect = ({
+ label,
value,
otherValue,
onSelect,
@@ -17,7 +19,7 @@ export const StatusSelect = ({
}: StatusSelectProps) => {
return (
- 현재 상태를 선택해주세요.
+ {label}
{STATE_OPTIONS.map((option) => (