Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d7e19f3
feat: 온보딩 페이지 이용약관 문구 UI 추가 (#159)
odukong Mar 20, 2026
2b90d5a
feat: 온보딩 유효성 검사 로직, 동의항목 추가 (#159)
odukong Mar 20, 2026
235820f
refactor: 모달 크기(width, height)를 커스텀하기 위한 size 속성 추가 (#159)
odukong Mar 20, 2026
4a403e8
design: 모달 X 버튼 사이즈 수정 (#159)
odukong Mar 20, 2026
2a2fb47
feat: 이용약관 텍스트 데이터 생성 (#159)
odukong Mar 20, 2026
405f84b
feat: 이용약관 모달 UI 구현 (#159)
odukong Mar 20, 2026
7ba628d
refactor: 이용약관&개인정보처리방침 모달 컨텐츠 공유를 위한 Content 컴포넌트 분리 (#159)
odukong Mar 21, 2026
4ad1dda
feat: 개인정보처리방침 UI 구현 (#159)
odukong Mar 21, 2026
4e6ad76
feat: 이용약관&개인정보처리방침 페이지 UI 구현 (#159)
odukong Mar 22, 2026
aa3002f
refactor: policypage 중복 코드 제거 (#159)
odukong Mar 22, 2026
3eb508f
fix: 코드래빗 수정사항 반영 (#159)
odukong Mar 23, 2026
421908a
design: 개인정보처리방침 페이지 gap 수정 (#159)
odukong Mar 24, 2026
dd9b4f9
refactor: 체크박스 이벤트와 약관 모달 이벤트 분리 (#159)
odukong Mar 25, 2026
2ff1fc6
design: 모달 컴포넌트 박스 영역 반응형 적용 (#159)
odukong Mar 25, 2026
06667ad
fix: 누락된 key 추가 (#159)
odukong Mar 25, 2026
0f6e5dd
refactor: 약관 모달 조건부 렌더링 로직을 객체 매핑 방식으로 개선 (#159)
odukong Mar 25, 2026
4658aea
design: 모달 아이콘 추가 (#159)
odukong Mar 26, 2026
6919883
design: medium 버튼 텍스트 크기 body_m_14로 변경 (#159)
odukong Mar 26, 2026
b246e98
design: Modal css 수정사항 반영 (#159)
odukong Mar 26, 2026
6649060
refactor: Modal css수정에 따른 코드 수정 (#159)
odukong Mar 26, 2026
7b0c65a
design: 코드래빗 수정사항 반영 (#159)
odukong Mar 27, 2026
ad92565
design: 모달 텍스트 띄어쓰기 추가 (#159)
odukong Mar 31, 2026
7d20951
fix: 카카오로그인 로직 수정 및 리프레쉬 토큰 오류 해결 (#159)
odukong Mar 31, 2026
c563804
Merge branch 'dev' into feat/#159/onboarding-agree-modal
odukong Mar 31, 2026
9a94bbd
fix: 빌드 에러 해결 (#159)
odukong Mar 31, 2026
e50c4b5
fix: ai-report 생성 중복 요청 에러 해결 (#159)
odukong Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/providers/modal-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ModalItem {
id: string;
content: ReactNode;
autoPlay?: number;
size?: "default" | "auto";
}

export const ModalProvider = () => {
Expand All @@ -33,6 +34,7 @@ export const ModalProvider = () => {
isOpen={true}
autoPlay={modal.autoPlay}
onClose={() => modalStore.close(modal.id)}
size={modal.size}
>
{modal.content}
</Modal>
Expand Down
3 changes: 3 additions & 0 deletions src/app/routes/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export const ROUTES = {
EXPERIENCE_DETAIL: (id = ":id") => `/experience/${id}`, // 경험 상세
EXPERIENCE_EDIT: (id = ":id") => `/experience/${id}/edit`, // 경험 수정

POLICY_USE: "/policy/terms", // 이용약관
POLICY_PRIVACY: "/policy/privacy", // 개인정보처리방침

MYPAGE: "/mypage",
};
8 changes: 8 additions & 0 deletions src/app/routes/public-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ const CompanyDetailPage = lazy(() =>
}))
);

const PolicyPage = lazy(() =>
import("@/pages/policy/policy-page").then((module) => ({
default: module.PolicyPage,
}))
);

export const guestRoutes = [{ path: ROUTES.LOGIN, element: <LoginPage /> }];

export const publicRoutes = [
{ path: ROUTES.LOGIN_AUTH, element: <KakaoLoginPage /> },
{ path: ROUTES.LANDING, element: <LandingPage /> },
{ path: ROUTES.HOME, element: <HomePage /> },
{ path: ROUTES.COMPANY(), element: <CompanyDetailPage /> },
{ path: ROUTES.POLICY_USE, element: <PolicyPage mode="USE" /> },
{ path: ROUTES.POLICY_PRIVACY, element: <PolicyPage mode="PRIVACY" /> },
];
14 changes: 8 additions & 6 deletions src/features/experience-detail/model/use-leave-confirm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useCallback } from "react";
import { useBlocker } from "react-router-dom";

import { IconWarn } from "@/shared/assets/icons";
import { modalStore } from "@/shared/model/store";
import { ModalBasic } from "@/shared/ui";

Expand Down Expand Up @@ -65,12 +66,13 @@ export const useLeaveConfirm = () => {
if (blocker.state === "blocked") {
modalStore.open(
<ModalBasic
title={`작성중인 내용이 있습니다.\n정말 나가시겠습니까?`}
subTitle="저장하지 않으면 내용이 사라져요."
closeText="이어서 작성"
confirmText="나가기"
onClose={cancelLeave}
onConfirm={confirmLeave}
icon={<IconWarn width={48} height={48} />}
title={`작성 중인 내용이 있어요`}
subTitle="저장하지 않으면 내용이 모두 사라져요."
closeText="나가기"
confirmText="계속 작성하기"
onClose={confirmLeave}
onConfirm={cancelLeave}
/>,
Comment on lines 68 to 76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

모달 버튼 의미와 XButton 동작 간 불일치 가능성이 있어요.

onCloseconfirmLeave(나가기)를, onConfirmcancelLeave(계속 작성)를 매핑한 것은 버튼 라벨과 일치하지만, Modal.XButton 클릭 시 동작이 예상과 다를 수 있어요.

modal-provider.tsx에서 ModalonClosemodalStore.close(modal.id)만 호출해요. 이 경우:

  1. XButton 클릭 → 모달만 닫힘 (blocker.proceed/reset 미호출)
  2. blocker.state가 여전히 "blocked" 상태
  3. useEffect가 다시 트리거되어 모달이 재오픈됨

기능적으로는 작동하지만, 사용자가 XButton으로 모달을 닫으려 할 때 즉시 다시 열리는 UX가 혼란스러울 수 있어요. XButton을 숨기거나, 명시적으로 "계속 작성하기" 동작과 연결하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/model/use-leave-confirm.tsx` around lines 68 -
76, The modal's XButton currently triggers ModalBasic.onClose which only closes
the modal (leaving blocker.state "blocked"), causing the modal to immediately
re-open; to fix, either hide the XButton or wire the XButton behavior to the
"continue editing" action: change ModalBasic's onClose prop to call cancelLeave
(same as onConfirm) instead of confirmLeave, or ensure onClose invokes the same
unblock/reset logic as cancelLeave so blocker.proceed/reset runs; update the
ModalBasic usage (props onClose/onConfirm and functions
confirmLeave/cancelLeave) accordingly to keep labels and behavior consistent.

0,
undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IconTrash } from "@/shared/assets/icons";
import {
EXPERIENCE_TYPE,
type ExperienceTypeCode,
Expand Down Expand Up @@ -48,15 +49,18 @@ const ExperienceViewer = () => {
const handleOpenDeleteModal = () => {
modalStore.open(
<ModalBasic
title="이 경험을 삭제하시겠습니까?"
subTitle="작성한 내용은 즉시 제거되며, 복구할 수 없습니다."
closeText="취소"
confirmText="삭제"
onClose={() => modalStore.reset()} // 취소 시 닫기
onConfirm={() => {
icon={<IconTrash width={48} height={48} />}
title="이 경험을 삭제할까요?"
subTitle="삭제하면 다시 복구할 수 없어요"
closeText="삭제하기"
confirmText="취소하기"
onClose={() => {
onClickDelete(); // 실제 삭제 동작
modalStore.reset(); // 모달 닫기
}}
onConfirm={() => {
modalStore.reset(); // 취소 시 닫기
}}
/>
);
};
Expand Down
48 changes: 27 additions & 21 deletions src/features/experience-matching/ui/analyzing/analyzing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,44 @@

import type { CustomErrorResponse } from "@/shared/api/generate/http-client";

let isRequesting = false;

export const Analyzing = ({ nextStep }: { nextStep: () => void }) => {
const { company, experience, jobDescription, setReportId } = useReportStore();
const { mutate } = useCreateReport();
const { mutateAsync } = useCreateReport();

// 에러 핸들링 (임시)
const [open, setOpen] = useState(false);
const [errorMsg, setErrorMsg] = useState("");

useEffect(() => {
mutate(
{
companyId: company?.id ?? 0,
experienceId: experience?.id ?? 0,
jobDescription: jobDescription,
},
{
onSuccess: (response) => {
setReportId(response?.id ?? 0);
nextStep();
},
onError: (error: CustomErrorResponse) => {
const serverMessage =
error.message || "리포트 생성 중 에러가 발생했습니다";
setErrorMsg(serverMessage);
setOpen(true);
if (isRequesting) return;
isRequesting = true;

const handleRequest = async () => {
try {
const response = await mutateAsync({
companyId: company?.id ?? 0,
experienceId: experience?.id ?? 0,
jobDescription: jobDescription,
});

setTimeout(() => setOpen(false), 3000);
},
setReportId(response?.id ?? 0);
nextStep();
} catch (err) {
const error = err as CustomErrorResponse;
const serverMessage =
error.message || "리포트 생성 중 에러가 발생했습니다";
setErrorMsg(serverMessage);
setOpen(true);
setTimeout(() => setOpen(false), 3000);
} finally {
isRequesting = false;
}
);
}, [nextStep, setReportId, mutate]);
};

handleRequest();
}, []);

Check warning on line 50 in src/features/experience-matching/ui/analyzing/analyzing.tsx

View workflow job for this annotation

GitHub Actions / build-and-deploy

React Hook useEffect has missing dependencies: 'company?.id', 'experience?.id', 'jobDescription', 'mutateAsync', 'nextStep', and 'setReportId'. Either include them or remove the dependency array. If 'nextStep' changes too often, find the parent component that defines it and wrap that definition in useCallback
return (
<>
<div className={styles.layout}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

import { ROUTES } from "@/app/routes/paths";
import { IconPen } from "@/shared/assets/icons";
import { modalStore } from "@/shared/model/store";
import { Button, Modal } from "@/shared/ui";
import { Modal, ModalBasic } from "@/shared/ui";
import {
useGetExperience,
useGetCompanyList,
Expand Down Expand Up @@ -36,23 +37,21 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
useEffect(() => {
if (data?.totalElements === 0) {
modalStore.open(
<>
<Modal.Content>
<Modal.Title>아직 등록된 경험이 없습니다</Modal.Title>
<Modal.SubTitle>지금 바로 경험을 등록하러 가볼까요?</Modal.SubTitle>
</Modal.Content>
<Modal.Buttons>
<Button variant="secondary" onClick={() => navigate(ROUTES.HOME)}>
나가기
</Button>
<Button
variant="primary"
onClick={() => navigate(ROUTES.EXPERIENCE_CREATE)}
>
이동하기
</Button>
</Modal.Buttons>
</>,
<ModalBasic
onClose={() => {
modalStore.close("NO-EXPERIENCE");
navigate(ROUTES.HOME);
}}
onConfirm={() => {
modalStore.close("NO-EXPERIENCE");
navigate(ROUTES.EXPERIENCE_CREATE);
}}
icon={<IconPen width={48} height={48} />}
title="아직 등록된 경험이 없어요"
subTitle="경험을 등록하고 AI매칭을 시작해보세요"
closeText="나중에 할게요"
confirmText="경험 등록하기"
/>,
undefined,
undefined,
"NO-EXPERIENCE"
Expand All @@ -65,13 +64,15 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
// 기업 선택 후, 대기하는 모달
modalStore.open(
<>
<Modal.Content type="auto">
<Modal.Title>
{josa(selectedCompany.name, "을/를")} 선택하셨습니다
</Modal.Title>
<Modal.SubTitle>기업분석 내용을 불러오는 중입니다.</Modal.SubTitle>
<Modal.Content>
<Modal.Image />
<Modal.TitleGroup>
<Modal.Title>
{josa(selectedCompany.name, "을/를")} 선택했어요
</Modal.Title>
<Modal.SubTitle>기업분석 내용을 불러오고 있어요</Modal.SubTitle>
</Modal.TitleGroup>
</Modal.Content>
<Modal.Image />
</>,
3000,
() => {
Expand Down
2 changes: 2 additions & 0 deletions src/features/onboarding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from "./store/interest-select/selectors";

export { useGetUniversity } from "./api/use-get-university.query";
export { usePostOnboarding } from "./api/use-post-onboarding.mutation";

export { PolicyModal } from "./ui/policy-modal/policy-modal";
6 changes: 4 additions & 2 deletions src/features/onboarding/lib/onboarding-form.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export const isOnboardingFormComplete = (params: {
selectedUniversity: SearchItem | null;
industry: Record<number, unknown>;
job: Record<number, unknown>;
isAgreed: boolean;
}) => {
const { selectedEducation, selectedUniversity, industry, job } = params;
const { selectedEducation, selectedUniversity, industry, job, isAgreed } =
params;

const hasEducation = Boolean(selectedEducation);
const hasUniversity = Boolean(selectedUniversity);

const hasIndustry1 = Boolean(industry[1]);
const hasJob1 = Boolean(job[1]);

return hasEducation && hasUniversity && hasIndustry1 && hasJob1;
return hasEducation && hasUniversity && hasIndustry1 && hasJob1 && isAgreed;
};
Loading
Loading