From 1b0d2b0e11e0c9d2d77e8ae6ad9ad9324ccb5732 Mon Sep 17 00:00:00 2001 From: mingo Date: Mon, 18 May 2026 13:55:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20jd=20=ED=99=95=EC=9D=B8=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20[JDDEV-48]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/mock-application/jd-review/page.tsx | 26 ++++ jobdri/components/common/AppShell.tsx | 2 +- .../components/common/badges/RequiredDot.tsx | 13 ++ jobdri/components/common/badges/index.ts | 1 + jobdri/components/common/header/Header.tsx | 6 +- .../components/common/input/InputAutoGrow.tsx | 12 +- .../common/progress/ProgressSidebar.tsx | 24 ++-- .../mock-application/JdReviewMain.tsx | 127 ++++++++++++++++++ 8 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 jobdri/app/mock-application/jd-review/page.tsx create mode 100644 jobdri/components/common/badges/RequiredDot.tsx create mode 100644 jobdri/components/mock-application/JdReviewMain.tsx diff --git a/jobdri/app/mock-application/jd-review/page.tsx b/jobdri/app/mock-application/jd-review/page.tsx new file mode 100644 index 0000000..1169795 --- /dev/null +++ b/jobdri/app/mock-application/jd-review/page.tsx @@ -0,0 +1,26 @@ +import Header from "@/components/common/header/Header"; +import { Footer } from "@/components/common/footer"; +import JdReviewMain from "@/components/mock-application/JdReviewMain"; + +export default function MockApplicationJdReviewPage() { + return ( +
+
+
+ +
+
+
+

+ 공고 내용을 확인하고 수정해주세요 +

+
+ +
+
+ +
+
+
+ ); +} diff --git a/jobdri/components/common/AppShell.tsx b/jobdri/components/common/AppShell.tsx index f5a4c07..a4fabdc 100644 --- a/jobdri/components/common/AppShell.tsx +++ b/jobdri/components/common/AppShell.tsx @@ -5,7 +5,7 @@ import { usePathname } from "next/navigation"; import Lnb from "@/components/common/lnb/Lnb"; import PageHeader from "@/components/common/PageHeader"; -const standaloneRoutes = new Set(["/login"]); +const standaloneRoutes = new Set(["/login", "/mock-application/jd-review"]); export default function AppShell({ children }: { children: ReactNode }) { const pathname = usePathname(); diff --git a/jobdri/components/common/badges/RequiredDot.tsx b/jobdri/components/common/badges/RequiredDot.tsx new file mode 100644 index 0000000..dd1a4a0 --- /dev/null +++ b/jobdri/components/common/badges/RequiredDot.tsx @@ -0,0 +1,13 @@ +interface RequiredDotProps { + label?: string; +} + +export function RequiredDot({ label = "필수 항목" }: RequiredDotProps) { + return ( + + ); +} diff --git a/jobdri/components/common/badges/index.ts b/jobdri/components/common/badges/index.ts index ad79c02..6c8c892 100644 --- a/jobdri/components/common/badges/index.ts +++ b/jobdri/components/common/badges/index.ts @@ -1 +1,2 @@ export { CompleteBadge } from "./CompleteBadge"; +export { RequiredDot } from "./RequiredDot"; diff --git a/jobdri/components/common/header/Header.tsx b/jobdri/components/common/header/Header.tsx index 5972e31..bc95c0c 100644 --- a/jobdri/components/common/header/Header.tsx +++ b/jobdri/components/common/header/Header.tsx @@ -76,7 +76,7 @@ export default function Header({
    {steps.map((step, index) => { const stepNumber = index + 1; - const reached = stepNumber <= currentStep; + const isCurrent = stepNumber === currentStep; return (
  1. { if (textareaRef.current) { + const nextHeight = maxHeight + ? Math.min(textareaRef.current.scrollHeight, maxHeight) + : textareaRef.current.scrollHeight; + textareaRef.current.style.height = "1px"; - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + textareaRef.current.style.height = `${nextHeight}px`; + textareaRef.current.style.overflowY = + maxHeight && textareaRef.current.scrollHeight > maxHeight + ? "auto" + : "hidden"; } - }, [value]); + }, [maxHeight, value]); return (
    diff --git a/jobdri/components/common/progress/ProgressSidebar.tsx b/jobdri/components/common/progress/ProgressSidebar.tsx index 0ceef2b..212b153 100644 --- a/jobdri/components/common/progress/ProgressSidebar.tsx +++ b/jobdri/components/common/progress/ProgressSidebar.tsx @@ -53,27 +53,31 @@ export default function ProgressSidebar({ let animationFrame = 0; const updateActiveFromScroll = () => { - const visibleTitles = itemIds + const activationLine = window.innerHeight * 0.3; + const sectionPositions = itemIds .map((id) => { const element = document.getElementById(id); if (!element) return null; const rect = element.getBoundingClientRect(); - const isVisible = rect.bottom > 0 && rect.top < window.innerHeight; - return isVisible ? { id, top: rect.top } : null; + return { + id, + top: rect.top, + }; }) .filter((item): item is { id: string; top: number } => Boolean(item)); - if (visibleTitles.length === 0) return; + if (sectionPositions.length === 0) return; - const topVisibleTitle = - visibleTitles - .filter((item) => item.top >= 0) - .sort((a, b) => a.top - b.top)[0] ?? - visibleTitles.sort((a, b) => b.top - a.top)[0]; + const passedSections = sectionPositions.filter( + (item) => item.top <= activationLine, + ); + const activeSection = + passedSections.sort((a, b) => b.top - a.top)[0] ?? + sectionPositions.sort((a, b) => a.top - b.top)[0]; - setActive(topVisibleTitle.id); + setActive(activeSection.id); }; const requestUpdate = () => { diff --git a/jobdri/components/mock-application/JdReviewMain.tsx b/jobdri/components/mock-application/JdReviewMain.tsx new file mode 100644 index 0000000..9b90a20 --- /dev/null +++ b/jobdri/components/mock-application/JdReviewMain.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import { ProgressSidebar } from "@/components/common/progress"; +import { InputAutoGrow } from "@/components/common/input"; +import { RequiredDot } from "@/components/common/badges"; + +export interface JdReviewSection { + id: string; + label: string; + value: string; + required?: boolean; +} + +export const mockJdSections: JdReviewSection[] = [ + { + id: "job", + label: "직무", + value: "리더십/조직개발 기업교육 컨설턴트 (HRD)", + required: true, + }, + { + id: "main-task", + label: "주요 업무", + value: + "기업 및 공공부문 및 교육 컨설팅\n- 기업 및 공공부문(B2B, B2G) 교육 컨설팅\n- 고객 니즈 기반 맞춤형 교육 솔루션 기획/제안\n- 제안서 작성, 고객사 미팅 비딩, 프레젠테이션\n- 리더십/조직개발/AI 트렌드 기반 콘텐츠 연구", + required: true, + }, + { + id: "qualification", + label: "자격요건", + value: + "2) 교육 운영 및 커뮤니케이션\n- 고객사 요청 프로젝트 일정 품질 관리(PM)\n- 현장 운영·사후 리포트·만족도 관리\n- 고객-강사-참여 및 커뮤니케이션 등\n\n3) 교육 콘텐츠 기획\n- 교육 자료 교안 구성·활동 설계 및 콘텐츠 리패키징(교육 콘텐츠 기획 및 설계)\n- 진단 및 학습도구(리더십 진단, 업무성향 진단 등)를 활용한 교육 프로그램 개발\n- 교수설계·학습경험 설계에 대한 이해와 관심", + required: true, + }, + { + id: "preference", + label: "우대사항", + value: + "- 공공기관(B2G) 교육사업 입찰 및 사업 경험자 우대\n- HRD·리더십 교육 분야에 대한 이해 필수\n- 문제 정의, 기획력, 대안 제시 능력이 뛰어난 사람\n- AI 기반 업무생산성 도구, 파워포인트를 포함한 MS Office 활용 능력", + required: true, + }, +]; + +function JdFieldLabel({ + label, + required, +}: { + label: string; + required?: boolean; +}) { + return ( +
    +
    +

    + {label} +

    + {required && } +
    +
    + ); +} + +function JdReviewField({ + section, + onChange, +}: { + section: JdReviewSection; + onChange: (value: string) => void; +}) { + return ( +
    +
    + + + +
    +
    + ); +} + +export default function JdReviewMain({ + sections: initialSections = mockJdSections, +}: { + sections?: JdReviewSection[]; +}) { + const [sections, setSections] = useState(initialSections); + const sidebarItems = sections.map(({ id, label }) => ({ id, label })); + + const updateSectionValue = (id: string, value: string) => { + setSections((currentSections) => + currentSections.map((section) => + section.id === id ? { ...section, value } : section, + ), + ); + }; + + return ( +
    +
    +
    + {sections.map((section) => ( + updateSectionValue(section.id, value)} + /> + ))} +
    +
    + + +
    + ); +} From 5fae3f00d13b492bbf16a991ab368aae224babd8 Mon Sep 17 00:00:00 2001 From: mingo Date: Mon, 18 May 2026 14:18:34 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20jd=20=ED=99=95=EC=9D=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=92=A4=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84=20[JDDEV-48?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jd-review/JdReviewPageClient.tsx | 61 +++++++++++++++++++ .../app/mock-application/jd-review/page.tsx | 25 +------- jobdri/components/common/AppShell.tsx | 6 +- .../components/common/input/InputAutoGrow.tsx | 3 +- .../components/common/modal/ModalNotice.tsx | 8 ++- .../mock-application/JdReviewMain.tsx | 25 ++------ 6 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 jobdri/app/mock-application/jd-review/JdReviewPageClient.tsx diff --git a/jobdri/app/mock-application/jd-review/JdReviewPageClient.tsx b/jobdri/app/mock-application/jd-review/JdReviewPageClient.tsx new file mode 100644 index 0000000..8e9f089 --- /dev/null +++ b/jobdri/app/mock-application/jd-review/JdReviewPageClient.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Header from "@/components/common/header/Header"; +import { Footer } from "@/components/common/footer"; +import { ModalNotice } from "@/components/common/modal"; +import JdReviewMain from "@/components/mock-application/JdReviewMain"; + +const JD_INPUT_PATH = "/mock-application/jd-input"; + +export default function JdReviewPageClient() { + const router = useRouter(); + const [showBackConfirm, setShowBackConfirm] = useState(false); + + const openBackConfirm = () => setShowBackConfirm(true); + const closeBackConfirm = () => setShowBackConfirm(false); + const goToJdInput = () => router.replace(JD_INPUT_PATH); + + return ( +
    +
    +
    + +
    +
    +
    +

    + 공고 내용을 확인하고 수정해주세요 +

    +
    + +
    +
    + +
    +
    + + {showBackConfirm && ( +
    + +
    + )} +
    + ); +} diff --git a/jobdri/app/mock-application/jd-review/page.tsx b/jobdri/app/mock-application/jd-review/page.tsx index 1169795..fe0b92e 100644 --- a/jobdri/app/mock-application/jd-review/page.tsx +++ b/jobdri/app/mock-application/jd-review/page.tsx @@ -1,26 +1,5 @@ -import Header from "@/components/common/header/Header"; -import { Footer } from "@/components/common/footer"; -import JdReviewMain from "@/components/mock-application/JdReviewMain"; +import JdReviewPageClient from "./JdReviewPageClient"; export default function MockApplicationJdReviewPage() { - return ( -
    -
    -
    - -
    -
    -
    -

    - 공고 내용을 확인하고 수정해주세요 -

    -
    - -
    -
    - -
    -
    -
    - ); + return ; } diff --git a/jobdri/components/common/AppShell.tsx b/jobdri/components/common/AppShell.tsx index a4fabdc..3f46101 100644 --- a/jobdri/components/common/AppShell.tsx +++ b/jobdri/components/common/AppShell.tsx @@ -5,7 +5,11 @@ import { usePathname } from "next/navigation"; import Lnb from "@/components/common/lnb/Lnb"; import PageHeader from "@/components/common/PageHeader"; -const standaloneRoutes = new Set(["/login", "/mock-application/jd-review"]); +const standaloneRoutes = new Set([ + "/login", + "/mock-application/jd-review", + "/mock-application/jd-input", +]); export default function AppShell({ children }: { children: ReactNode }) { const pathname = usePathname(); diff --git a/jobdri/components/common/input/InputAutoGrow.tsx b/jobdri/components/common/input/InputAutoGrow.tsx index 252af58..9e8cd22 100644 --- a/jobdri/components/common/input/InputAutoGrow.tsx +++ b/jobdri/components/common/input/InputAutoGrow.tsx @@ -40,11 +40,12 @@ export function InputAutoGrow({ useLayoutEffect(() => { if (textareaRef.current) { + textareaRef.current.style.height = "1px"; + const nextHeight = maxHeight ? Math.min(textareaRef.current.scrollHeight, maxHeight) : textareaRef.current.scrollHeight; - textareaRef.current.style.height = "1px"; textareaRef.current.style.height = `${nextHeight}px`; textareaRef.current.style.overflowY = maxHeight && textareaRef.current.scrollHeight > maxHeight diff --git a/jobdri/components/common/modal/ModalNotice.tsx b/jobdri/components/common/modal/ModalNotice.tsx index b855660..fc3e07e 100644 --- a/jobdri/components/common/modal/ModalNotice.tsx +++ b/jobdri/components/common/modal/ModalNotice.tsx @@ -3,6 +3,7 @@ import clsx from "clsx"; import { Button } from "@/components/common/buttons"; type ModalNoticeVariant = "single" | "double"; +type ModalNoticeType = "notice" | "confirmationModal"; interface ModalNoticeActionProps extends Omit, "children"> { @@ -10,6 +11,7 @@ interface ModalNoticeActionProps } interface ModalNoticeProps { + type?: ModalNoticeType; variant?: ModalNoticeVariant; title?: string; description?: string; @@ -19,6 +21,7 @@ interface ModalNoticeProps { } export default function ModalNotice({ + type = "notice", variant = "single", title = "공고 링크를 입력해주세요.", description = "링크 내용이 부적절한 경우 제대로 추출되지 않을 수 있습니다.", @@ -26,8 +29,9 @@ export default function ModalNotice({ secondaryAction = {}, className, }: ModalNoticeProps) { + const resolvedVariant = type === "confirmationModal" ? "double" : variant; const { - label: primaryLabel = variant === "single" ? "닫기" : "입력하기", + label: primaryLabel = resolvedVariant === "single" ? "닫기" : "입력하기", className: primaryClassName, ...primaryButtonProps } = primaryAction; @@ -63,7 +67,7 @@ export default function ModalNotice({
    - {variant === "single" ? ( + {resolvedVariant === "single" ? (
    ); } @@ -74,7 +59,7 @@ function JdReviewField({ className="flex scroll-mt-8 flex-col items-center justify-center gap-8 self-stretch rounded-card-l bg-fill-quaternary-default px-7 pt-6 pb-7 shadow-card" >
    - +