-
Notifications
You must be signed in to change notification settings - Fork 1
[JDDEV-89] Feature: home page refactoring #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb7ac2f
6a6d0e0
7993314
83b3c1d
aa002f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { | |||||||||||||||||||||||||||||||||||
| useMemo, | ||||||||||||||||||||||||||||||||||||
| useState, | ||||||||||||||||||||||||||||||||||||
| useEffect, | ||||||||||||||||||||||||||||||||||||
| useRef, | ||||||||||||||||||||||||||||||||||||
| type MouseEvent, | ||||||||||||||||||||||||||||||||||||
| type ReactNode, | ||||||||||||||||||||||||||||||||||||
| } from "react"; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -34,6 +35,7 @@ import { | |||||||||||||||||||||||||||||||||||
| createApplyFromJobPosting, | ||||||||||||||||||||||||||||||||||||
| getSelectedApplyType, | ||||||||||||||||||||||||||||||||||||
| } from "@/lib/api/mockApplies"; | ||||||||||||||||||||||||||||||||||||
| import { useDebounce } from "@/hooks/useDebounce"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function firstNonEmpty(...values: Array<string | null | undefined>) { | ||||||||||||||||||||||||||||||||||||
| return values.find((value) => value?.trim())?.trim() ?? ""; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -142,6 +144,75 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| const [isSaving, setIsSaving] = useState(false); | ||||||||||||||||||||||||||||||||||||
| const [saveErrorMessage, setSaveErrorMessage] = useState(""); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const [lastSavedTime, setLastSavedTime] = useState<string>(""); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const isInitialRender = useRef(true); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const currentPayload: JobPostingSavePayload = useMemo( | ||||||||||||||||||||||||||||||||||||
| () => ({ | ||||||||||||||||||||||||||||||||||||
| profileColor, | ||||||||||||||||||||||||||||||||||||
| postingName: jobPostingName.trim(), | ||||||||||||||||||||||||||||||||||||
| companyName: companyName.trim(), | ||||||||||||||||||||||||||||||||||||
| companySize: companySize, | ||||||||||||||||||||||||||||||||||||
| jobTitle: roleName.trim(), | ||||||||||||||||||||||||||||||||||||
| detailClassificationId: detailClassificationId, | ||||||||||||||||||||||||||||||||||||
| task: task.trim(), | ||||||||||||||||||||||||||||||||||||
| requirement: requirements.trim(), | ||||||||||||||||||||||||||||||||||||
| preferred: preferred.trim(), | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||
| profileColor, | ||||||||||||||||||||||||||||||||||||
| jobPostingName, | ||||||||||||||||||||||||||||||||||||
| companyName, | ||||||||||||||||||||||||||||||||||||
| companySize, | ||||||||||||||||||||||||||||||||||||
| roleName, | ||||||||||||||||||||||||||||||||||||
| detailClassificationId, | ||||||||||||||||||||||||||||||||||||
| task, | ||||||||||||||||||||||||||||||||||||
| requirements, | ||||||||||||||||||||||||||||||||||||
| preferred, | ||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const debouncedPayload = useDebounce(currentPayload, 1000); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| isLoading || | ||||||||||||||||||||||||||||||||||||
| debouncedPayload.detailClassificationId <= 0 || | ||||||||||||||||||||||||||||||||||||
| !debouncedPayload.postingName || | ||||||||||||||||||||||||||||||||||||
| !debouncedPayload.companyName || | ||||||||||||||||||||||||||||||||||||
| !debouncedPayload.jobTitle | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (isInitialRender.current) { | ||||||||||||||||||||||||||||||||||||
| isInitialRender.current = false; | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const autoSave = async () => { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| let savedResult; | ||||||||||||||||||||||||||||||||||||
| if (jobPostingId) { | ||||||||||||||||||||||||||||||||||||
| savedResult = await updateJobPosting(jobPostingId, debouncedPayload); | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| savedResult = await saveJobPosting(debouncedPayload); | ||||||||||||||||||||||||||||||||||||
| setJobPostingId(savedResult.jobPostingId); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||||||||||||||||||||
| setLastSavedTime( | ||||||||||||||||||||||||||||||||||||
| `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||
| console.error("μ±μ© κ³΅κ³ μλ μ μ₯ μ€ν¨:", error); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| autoSave(); | ||||||||||||||||||||||||||||||||||||
| }, [debouncedPayload, jobPostingId, isLoading]); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const { scrollAreaRef, scrollbarMetrics, updateScrollbarMetrics } = | ||||||||||||||||||||||||||||||||||||
| useLnbScrollMetrics(true, "job-posting-review", { trackPadding: 28 }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -158,11 +229,11 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| [companyName, jobPostingName, roleName], | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // 1. μ΄κΈ° λ°μ΄ν° λ‘λ© | ||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||
| const loadData = async () => { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| if (urlJobPostingId) { | ||||||||||||||||||||||||||||||||||||
| // URL κ²½λ‘μ IDκ° μμΌλ©΄ DBμμ ν¨μΉ | ||||||||||||||||||||||||||||||||||||
| const saved = await fetchMyJobPosting(Number(urlJobPostingId)); | ||||||||||||||||||||||||||||||||||||
| setProfileColor(saved.profileColor ?? "DEFAULT"); | ||||||||||||||||||||||||||||||||||||
| setJobPostingName( | ||||||||||||||||||||||||||||||||||||
|
|
@@ -183,7 +254,6 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| setDetailClassificationId(saved.detailClassificationId ?? 0); | ||||||||||||||||||||||||||||||||||||
| setJobPostingId(saved.jobPostingId ?? null); | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| // νλΌλ―Έν°κ° μμΌλ©΄ μ€ν μ΄μμ μμ λ°μ΄ν° ν¨μΉ | ||||||||||||||||||||||||||||||||||||
| const result = getJobPostingAnalysis(); | ||||||||||||||||||||||||||||||||||||
| if (!result) { | ||||||||||||||||||||||||||||||||||||
| router.replace("/mockApply/job/create"); | ||||||||||||||||||||||||||||||||||||
|
|
@@ -246,6 +316,10 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| router.replace("/"); | ||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||
| setIsLoading(false); | ||||||||||||||||||||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||||||||||||||||||||
| setLastSavedTime( | ||||||||||||||||||||||||||||||||||||
| `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -268,21 +342,9 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| throw new Error("μ§λ¬΄ λΆλ₯ μ λ³΄κ° μμ΄ κ³΅κ³ λ₯Ό μ μ₯ν μ μμ΅λλ€."); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const payload: JobPostingSavePayload = { | ||||||||||||||||||||||||||||||||||||
| profileColor, | ||||||||||||||||||||||||||||||||||||
| postingName: jobPostingName.trim(), | ||||||||||||||||||||||||||||||||||||
| companyName: companyName.trim(), | ||||||||||||||||||||||||||||||||||||
| companySize: companySize, | ||||||||||||||||||||||||||||||||||||
| jobTitle: roleName.trim(), | ||||||||||||||||||||||||||||||||||||
| detailClassificationId: detailClassificationId, | ||||||||||||||||||||||||||||||||||||
| task: task.trim(), | ||||||||||||||||||||||||||||||||||||
| requirement: requirements.trim(), | ||||||||||||||||||||||||||||||||||||
| preferred: preferred.trim(), | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const savedJobPosting = jobPostingId | ||||||||||||||||||||||||||||||||||||
| ? await updateJobPosting(jobPostingId, payload) | ||||||||||||||||||||||||||||||||||||
| : await saveJobPosting(payload); | ||||||||||||||||||||||||||||||||||||
| ? await updateJobPosting(jobPostingId, currentPayload) | ||||||||||||||||||||||||||||||||||||
| : await saveJobPosting(currentPayload); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
345
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ποΈ Data Integrity & Integration | π΄ Critical | β‘ Quick win Missing Unlike the autosave branch (line 201), this "create new" path never calls π§ Proposed fix const savedJobPosting = jobPostingId
? await updateJobPosting(jobPostingId, currentPayload)
: await saveJobPosting(currentPayload);
+ if (!jobPostingId) {
+ setJobPostingId(savedJobPosting.jobPostingId);
+ }
+
const createdApply = await createApplyFromJobPosting({
jobPostingId: savedJobPosting.jobPostingId,
applyType: getSelectedApplyType(),
});π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const createdApply = await createApplyFromJobPosting({ | ||||||||||||||||||||||||||||||||||||
| jobPostingId: savedJobPosting.jobPostingId, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -322,7 +384,7 @@ export default function JobPostingReviewPage() { | |||||||||||||||||||||||||||||||||||
| applicationLabel="첫 λ²μ§Έ μ§μ" | ||||||||||||||||||||||||||||||||||||
| currentStep={1} | ||||||||||||||||||||||||||||||||||||
| steps={wizardSteps} | ||||||||||||||||||||||||||||||||||||
| lastSavedAt="17:00" | ||||||||||||||||||||||||||||||||||||
| lastSavedAt={lastSavedTime} | ||||||||||||||||||||||||||||||||||||
| homeAction={{ label: "νμΌλ‘", onClick: handleHomeClick }} | ||||||||||||||||||||||||||||||||||||
| className="min-w-[1100px] max-w-none shrink-0 self-stretch" | ||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useEffect } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { Button } from "@/components/common/buttons"; | ||
|
|
@@ -16,12 +17,13 @@ import { | |
| fetchMyJobPostings, | ||
| } from "@/lib/api/jobPostings"; | ||
| import { saveJobPostingAnalysis } from "@/app/mockApply/job/jobPostingDraftStore"; | ||
| import { formatDate } from "@/utils/date"; | ||
| import { formatDate, formatRelativeDate } from "@/utils/date"; | ||
| import { | ||
| DraftData, | ||
| ApplicationCardData, | ||
| } from "@/components/mockApply/home/types"; | ||
| import { useReApply } from "@/hooks/useReApply"; | ||
| import { mapMockApplyToApplication } from "@/components/mockApply/home/applicationHomeUtils"; | ||
|
|
||
| export default function Home() { | ||
| const router = useRouter(); | ||
|
|
@@ -32,18 +34,27 @@ export default function Home() { | |
| useEffect(() => { | ||
| const loadMockApplies = async () => { | ||
| try { | ||
| const [data, jobPostings] = await Promise.all([ | ||
| const [data, fetchedJobPostings] = await Promise.all([ | ||
| fetchMyMockApplies({ redirectOnUnauthorized: false }), | ||
| fetchMyJobPostings({ redirectOnUnauthorized: false }).catch(() => []), | ||
| ]); | ||
|
|
||
| const jobPostings = Array.isArray(fetchedJobPostings) | ||
| ? fetchedJobPostings | ||
| : []; | ||
|
|
||
| const inProgressList = data?.inProgress || []; | ||
| const completedList = data?.completed?.content || []; | ||
|
Comment on lines
+37
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π Major | β‘ Quick win π§© Analysis chainπ Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)jobdri/src/(app/page\.tsx|lib/api/(jobPostings|mockApplies)\.ts)$' || true
echo "== page outline =="
ast-grep outline jobdri/src/app/page.tsx --view compact || true
echo "== relevant page lines =="
sed -n '1,90p' jobdri/src/app/page.tsx
echo "== jobPostings relevant lines =="
sed -n '1,220p' jobdri/src/lib/api/jobPostings.ts
sed -n '220,420p' jobdri/src/lib/api/jobPostings.ts
echo "== mockApplies relevant lines =="
sed -n '1,180p' jobdri/src/lib/api/mockApplies.ts
echo "== API pagination references =="
rg -n "PaginatedResponse|content|page|size|size=|page=" jobdri/src -S || trueRepository: JobDri-Developer/FrontEnd Length of output: 49837 π Web query:
π‘ Result: In the Spring Framework, Citations:
Load every paged result before rendering the dashboard. Both Home data fetchers return page metadata ( π Affects 3 files
π€ Prompt for AI Agents |
||
|
|
||
| const jobPostingById = new Map( | ||
| jobPostings.map((jobPosting) => [ | ||
| jobPosting.jobPostingId, | ||
| jobPosting, | ||
| ]), | ||
| ); | ||
|
|
||
| const mappedDrafts = data.inProgress.map((item) => { | ||
| // π μμ± μ€μΈ λͺ¨μμ§μ(Drafts) λ§€ν | ||
| const mappedDrafts = inProgressList.map((item) => { | ||
| const jobPosting = jobPostingById.get(item.jobPostingId); | ||
|
|
||
| return { | ||
|
|
@@ -60,14 +71,19 @@ export default function Home() { | |
| jobPosting?.detailClassificationName || | ||
| "μ§λ¬΄ λ―Έμ§μ ", | ||
| currentStep: item.status === "ANSWER_WRITE" ? 2 : 1, | ||
| updatedAt: formatDate(item.createdAt), | ||
| updatedAt: item.createdAt | ||
| ? formatRelativeDate(item.createdAt) | ||
| : "-", | ||
| }; | ||
| }); | ||
|
|
||
| const linkedJobPostingIds = new Set( | ||
| [...data.inProgress, ...data.completed].map( | ||
| [...inProgressList, ...completedList].map( | ||
| (item) => item.jobPostingId, | ||
| ), | ||
| ); | ||
|
|
||
| // π μμ μ±μ© κ³΅κ³ (Drafts) λ§€ν | ||
| const savedOnlyDrafts = jobPostings | ||
| .filter( | ||
| (jobPosting) => !linkedJobPostingIds.has(jobPosting.jobPostingId), | ||
|
|
@@ -82,31 +98,30 @@ export default function Home() { | |
| jobPosting.detailClassificationName || | ||
| "μ§λ¬΄ λ―Έμ§μ ", | ||
| currentStep: 1, | ||
| updatedAt: "-", | ||
| updatedAt: jobPosting.createdAt | ||
| ? formatRelativeDate(jobPosting.createdAt) | ||
| : "-", | ||
| })); | ||
|
|
||
| const mappedResults = data.completed.map((item) => { | ||
| const mappedResults = completedList.map((item) => { | ||
| const jobPosting = jobPostingById.get(item.jobPostingId); | ||
|
|
||
| return { | ||
| id: item.mockApplyId, | ||
| jobPostingId: item.jobPostingId, | ||
| mockApplyId: item.mockApplyId, | ||
| company: | ||
| item.companyName || jobPosting?.companyName || "νμ¬λͺ λ―Έμ λ ₯", | ||
| profileColor: jobPosting?.profileColor ?? "DEFAULT", | ||
| position: | ||
| item.jobTitle || | ||
| jobPosting?.jobTitle || | ||
| item.detailClassificationName || | ||
| jobPosting?.detailClassificationName || | ||
| "μ§λ¬΄ λ―Έμ§μ ", | ||
| createdAt: formatDate(item.createdAt), | ||
| score: item.score || 0, | ||
| version: item.version || 1, | ||
| status: "completed", | ||
| }; | ||
| const cardData = mapMockApplyToApplication( | ||
| { | ||
| ...item, | ||
| profileColor: jobPosting?.profileColor ?? "DEFAULT", | ||
| jobTitle: item.jobTitle || jobPosting?.jobTitle || "", | ||
| detailClassificationName: | ||
| item.detailClassificationName || | ||
| jobPosting?.detailClassificationName || | ||
| "", | ||
| }, | ||
| "completed", | ||
| ); | ||
|
|
||
| return cardData; | ||
| }); | ||
|
|
||
| setDrafts([...savedOnlyDrafts, ...mappedDrafts]); | ||
| setResults(mappedResults); | ||
| } catch (error) { | ||
|
|
@@ -130,6 +145,7 @@ export default function Home() { | |
| console.error("μ±μ© κ³΅κ³ λ₯Ό μμ νμ§ λͺ»νμ΅λλ€.", error); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen w-full bg-[#F5F6F9] overflow-x-hidden "> | ||
| <Lnb className="shrink-0 z-50" /> | ||
|
|
@@ -179,7 +195,9 @@ export default function Home() { | |
| generated: null, | ||
| saved, | ||
| }); | ||
| router.push("/mockApply/job/review"); | ||
| router.push( | ||
| `/mockApply/job/${targetDraft.jobPostingId}/review`, | ||
| ); | ||
| }) | ||
| .catch((error) => { | ||
| console.error("μ±μ© κ³΅κ³ λ₯Ό λΆλ¬μ€μ§ λͺ»νμ΅λλ€.", error); | ||
|
|
@@ -191,7 +209,7 @@ export default function Home() { | |
| case 1: | ||
| case 2: | ||
| router.push( | ||
| `/mockApply/${targetDraft.mockApplyId}?jobPostingId=${targetDraft.jobPostingId}`, | ||
| `/mockApply/job/${targetDraft.jobPostingId}/review`, | ||
| ); | ||
| break; | ||
| case 3: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Performance & Scalability | π Major | β‘ Quick win
jobPostingIdin the effect's dependency list causes a guaranteed duplicate save right after creating a new posting.When a new posting is created inside
autoSave(line 200),setJobPostingIdfires and β becausejobPostingIdis a dependency of this same effect β re-triggers it with the unchangeddebouncedPayload. SinceisInitialRender.currentis alreadyfalse, this immediately issues a second, redundantupdateJobPostingcall with identical data right after thesaveJobPostingPOST. This happens on every first-time autosave, not just an edge case.π§ Proposed fix β read `jobPostingId` via a ref instead of a dependency
π Committable suggestion
π€ Prompt for AI Agents