Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 61 additions & 0 deletions jobdri/app/mock-application/jd-review/JdReviewPageClient.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-line-neutral-assistive px-6 py-6">
<div className="mx-auto flex w-[1280px] flex-col">
<Header currentStep={3} />

<section className="flex flex-col items-center bg-bg-default px-[82px] pt-10 pb-18">
<div className="flex max-w-[1440px] flex-col items-center gap-8 self-stretch">
<div className="flex items-center justify-center gap-2.5 self-stretch">
<h2 className="text-center text-h24-bold text-text-neutral-title [font-feature-settings:'liga'_off,'clig'_off]">
공고 내용을 확인하고 수정해주세요
</h2>
</div>
<JdReviewMain />
</div>
</section>

<Footer
backAction={{ onClick: openBackConfirm }}
ctaAction={{ label: "확정하기" }}
/>
</div>

{showBackConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-bg-lightbox-default">
<ModalNotice
type="confirmationModal"
title="공고 내용을 다시 업로드 하시겠습니까?"
description="기존 정보는 저장되지 않고 삭제됩니다."
secondaryAction={{
label: "다시 업로드",
onClick: goToJdInput,
}}
primaryAction={{
label: "계속 작성",
onClick: closeBackConfirm,
}}
/>
</div>
)}
</div>
);
}
5 changes: 5 additions & 0 deletions jobdri/app/mock-application/jd-review/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import JdReviewPageClient from "./JdReviewPageClient";

export default function MockApplicationJdReviewPage() {
return <JdReviewPageClient />;
}
6 changes: 5 additions & 1 deletion jobdri/components/common/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
const standaloneRoutes = new Set([
"/login",
"/mock-application/jd-review",
"/mock-application/jd-input",
]);

export default function AppShell({ children }: { children: ReactNode }) {
const pathname = usePathname();
Expand Down
13 changes: 13 additions & 0 deletions jobdri/components/common/badges/RequiredDot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface RequiredDotProps {
label?: string;
}

export function RequiredDot({ label = "필수 항목" }: RequiredDotProps) {
return (
<span
aria-label={label}
role="img"
className="block h-[5px] min-h-[5px] w-[5px] min-w-[5px] shrink-0 rounded-[5px] border border-[#FF4242] bg-[#FF4545] box-border"
/>
);
}
1 change: 1 addition & 0 deletions jobdri/components/common/badges/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { CompleteBadge } from "./CompleteBadge";
export { RequiredDot } from "./RequiredDot";
6 changes: 3 additions & 3 deletions jobdri/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function Header({
<ol className="flex min-w-0 flex-1 flex-nowrap content-center items-center gap-2">
{steps.map((step, index) => {
const stepNumber = index + 1;
const reached = stepNumber <= currentStep;
const isCurrent = stepNumber === currentStep;

return (
<li
Expand All @@ -87,7 +87,7 @@ export default function Header({
className={clsx(
"flex aspect-square h-5 w-5 items-center justify-center gap-2.5 rounded-icon-round text-cap12-med [font-feature-settings:'liga'_off,'clig'_off]",
"tracking-normal",
reached
isCurrent
? "bg-fill-quaternary-default text-text-neutral-description shadow-cta-primary"
: "bg-fill-disabled text-text-neutral-disabled",
)}
Expand All @@ -98,7 +98,7 @@ export default function Header({
className={clsx(
"flex items-center justify-center gap-2.5 text-cap12-med [font-feature-settings:'liga'_off,'clig'_off]",
"tracking-normal",
reached
isCurrent
? "text-text-neutral-description"
: "text-text-neutral-disabled",
)}
Expand Down
13 changes: 11 additions & 2 deletions jobdri/components/common/input/InputAutoGrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ export function InputAutoGrow({
useLayoutEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "1px";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;

const nextHeight = maxHeight
? Math.min(textareaRef.current.scrollHeight, maxHeight)
: textareaRef.current.scrollHeight;

textareaRef.current.style.height = `${nextHeight}px`;
textareaRef.current.style.overflowY =
maxHeight && textareaRef.current.scrollHeight > maxHeight
? "auto"
: "hidden";
}
}, [value]);
}, [maxHeight, value]);

return (
<div className={clsx("flex flex-col gap-1.5 min-w-148", className)}>
Expand Down
8 changes: 6 additions & 2 deletions jobdri/components/common/modal/ModalNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import clsx from "clsx";
import { Button } from "@/components/common/buttons";

type ModalNoticeVariant = "single" | "double";
type ModalNoticeType = "notice" | "confirmationModal";

interface ModalNoticeActionProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
label?: string;
}

interface ModalNoticeProps {
type?: ModalNoticeType;
variant?: ModalNoticeVariant;
title?: string;
description?: string;
Expand All @@ -19,15 +21,17 @@ interface ModalNoticeProps {
}

export default function ModalNotice({
type = "notice",
variant = "single",
title = "공고 링크를 입력해주세요.",
description = "링크 내용이 부적절한 경우 제대로 추출되지 않을 수 있습니다.",
primaryAction = {},
secondaryAction = {},
className,
}: ModalNoticeProps) {
const resolvedVariant = type === "confirmationModal" ? "double" : variant;
const {
label: primaryLabel = variant === "single" ? "닫기" : "입력하기",
label: primaryLabel = resolvedVariant === "single" ? "닫기" : "입력하기",
className: primaryClassName,
...primaryButtonProps
} = primaryAction;
Expand Down Expand Up @@ -63,7 +67,7 @@ export default function ModalNotice({
</div>

<div className="flex self-stretch flex-col items-start gap-2.5 px-8 pb-8">
{variant === "single" ? (
{resolvedVariant === "single" ? (
<Button
label={primaryLabel}
styleType="secondary"
Expand Down
24 changes: 14 additions & 10 deletions jobdri/components/common/progress/ProgressSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
112 changes: 112 additions & 0 deletions jobdri/components/mock-application/JdReviewMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";

import { useState } from "react";
import { ProgressSidebar } from "@/components/common/progress";
import { InputAutoGrow } from "@/components/common/input";

export interface JdReviewSection {
id: string;
label: string;
value: string;
}

export const mockJdSections: JdReviewSection[] = [
{
id: "job",
label: "직무",
value: "리더십/조직개발 기업교육 컨설턴트 (HRD)",
},
{
id: "main-task",
label: "주요 업무",
value:
"기업 및 공공부문 및 교육 컨설팅\n- 기업 및 공공부문(B2B, B2G) 교육 컨설팅\n- 고객 니즈 기반 맞춤형 교육 솔루션 기획/제안\n- 제안서 작성, 고객사 미팅 비딩, 프레젠테이션\n- 리더십/조직개발/AI 트렌드 기반 콘텐츠 연구",
},
{
id: "qualification",
label: "자격요건",
value:
"2) 교육 운영 및 커뮤니케이션\n- 고객사 요청 프로젝트 일정 품질 관리(PM)\n- 현장 운영·사후 리포트·만족도 관리\n- 고객-강사-참여 및 커뮤니케이션 등\n\n3) 교육 콘텐츠 기획\n- 교육 자료 교안 구성·활동 설계 및 콘텐츠 리패키징(교육 콘텐츠 기획 및 설계)\n- 진단 및 학습도구(리더십 진단, 업무성향 진단 등)를 활용한 교육 프로그램 개발\n- 교수설계·학습경험 설계에 대한 이해와 관심",
},
{
id: "preference",
label: "우대사항",
value:
"- 공공기관(B2G) 교육사업 입찰 및 사업 경험자 우대\n- HRD·리더십 교육 분야에 대한 이해 필수\n- 문제 정의, 기획력, 대안 제시 능력이 뛰어난 사람\n- AI 기반 업무생산성 도구, 파워포인트를 포함한 MS Office 활용 능력",
},
];

function JdFieldLabel({ label }: { label: string }) {
return (
<div className="flex h-[29px] items-center self-stretch py-1 pr-0 pl-0.5">
<h3 className="min-w-0 truncate text-b16-semibold text-text-neutral-title [font-feature-settings:'liga'_off,'clig'_off]">
{label}
</h3>
</div>
);
}

function JdReviewField({
section,
onChange,
}: {
section: JdReviewSection;
onChange: (value: string) => void;
}) {
return (
<section
id={section.id}
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"
>
<div className="flex flex-col items-start gap-2 self-stretch">
<JdFieldLabel label={section.label} />

<InputAutoGrow
value={section.value}
onChange={onChange}
maxHeight={168}
className="w-full min-w-0"
/>
</div>
</section>
);
}

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 (
<main className="flex items-start gap-6 self-stretch">
<div className="flex flex-1 flex-col items-start gap-3 self-stretch">
<div className="flex flex-1 flex-col items-start gap-3 self-stretch">
{sections.map((section) => (
<JdReviewField
key={section.id}
section={section}
onChange={(value) => updateSectionValue(section.id, value)}
/>
))}
</div>
</div>

<ProgressSidebar
items={sidebarItems}
defaultActiveId={sections[0]?.id}
className="sticky top-6 h-auto shrink-0"
/>
</main>
);
}