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
157 changes: 99 additions & 58 deletions jobdri/src/app/mockApply/job/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState, type MouseEvent } from "react";
import { useEffect, useState, useRef, type MouseEvent } from "react";
import { useRouter } from "next/navigation";
import Header from "@/components/common/header/Header";
import { LLMInput } from "@/components/common/input";
Expand Down Expand Up @@ -86,48 +86,82 @@ export default function JobPostingCreatePage() {
const [showExitConfirm, setShowExitConfirm] = useState(false);
const [showInvalidDataModal, setShowInvalidDataModal] = useState(false);

const [jobPostingToastMessage, setJobPostingToastMessage] = useState<
string | null
>(null);
const [toastMessages, setToastMessages] = useState<string[]>([]);

const inputContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleClickOutside(event: globalThis.MouseEvent) {
if (
inputContainerRef.current &&
!inputContainerRef.current.contains(event.target as Node)
) {
const hasInput =
jobPostingInputValue.trim().length > 0 || attachedFiles.length > 0;

// 아무것도 입력되지 않았을 때만 원래대로 돌아감
if (!hasInput) {
setIsInputActive(false);
}
}
}

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [jobPostingInputValue, attachedFiles]); // 의존성 배열에 상태 추가

// ✅ 교체할 부분: URL에서 여러 개의 에러를 가져오도록 useEffect 수정
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const analysisError = searchParams.get("analysisError");

if (analysisError === "not_saved") {
// getAll을 사용하여 배열 형태로 모두 가져옵니다
const analysisErrors = searchParams.getAll("analysisError");
const isCanceled = searchParams.get("analysisCanceled");

if (analysisErrors.includes("not_saved")) {
const modalTimer = window.setTimeout(() => {
setShowInvalidDataModal(true);
window.history.replaceState(null, "", window.location.pathname);
}, 0);

return () => window.clearTimeout(modalTimer);
}

const toastMessage =
searchParams.get("analysisCanceled") === "1"
? "공고 분석을 중단했습니다."
: analysisError === "1"
? "업로드에 실패했습니다."
: analysisError;
const nextToastMessages: string[] = [];

if (isCanceled === "1") {
nextToastMessages.push("공고 분석을 중단했습니다.");
}

analysisErrors.forEach((err) => {
if (err === "1") nextToastMessages.push("업로드에 실패했습니다.");
else if (err !== "not_saved") nextToastMessages.push(err);
});

if (!toastMessage) {
if (nextToastMessages.length === 0) {
return;
}

const openToastTimer = window.setTimeout(() => {
setJobPostingToastMessage(toastMessage);
setToastMessages(nextToastMessages);
window.history.replaceState(null, "", window.location.pathname);
}, 0);

const toastTimer = window.setTimeout(() => {
setJobPostingToastMessage(null);
}, 3000);
setToastMessages([]);
}, 4000);

return () => {
window.clearTimeout(openToastTimer);
window.clearTimeout(toastTimer);
};
}, []);
Comment on lines +115 to 159

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Query params survive when there are no toasts to show.

The early return at line 142 skips history.replaceState, so a stray ?analysisError= (e.g. the empty-array case from loading/page.tsx) stays in the URL and is re-read on any remount. Clear the params before the early return.

🩹 Proposed fix
+    if (analysisErrors.length > 0 || isCanceled) {
+      window.history.replaceState(null, "", window.location.pathname);
+    }
+
     if (nextToastMessages.length === 0) {
       return;
     }
 
     const openToastTimer = window.setTimeout(() => {
       setToastMessages(nextToastMessages);
-      window.history.replaceState(null, "", window.location.pathname);
     }, 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ✅ 교체할 부분: URL에서 여러 개의 에러를 가져오도록 useEffect 수정
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
const analysisError = searchParams.get("analysisError");
if (analysisError === "not_saved") {
// getAll을 사용하여 배열 형태로 모두 가져옵니다
const analysisErrors = searchParams.getAll("analysisError");
const isCanceled = searchParams.get("analysisCanceled");
if (analysisErrors.includes("not_saved")) {
const modalTimer = window.setTimeout(() => {
setShowInvalidDataModal(true);
window.history.replaceState(null, "", window.location.pathname);
}, 0);
return () => window.clearTimeout(modalTimer);
}
const toastMessage =
searchParams.get("analysisCanceled") === "1"
? "공고 분석을 중단했습니다."
: analysisError === "1"
? "업로드에 실패했습니다."
: analysisError;
const nextToastMessages: string[] = [];
if (isCanceled === "1") {
nextToastMessages.push("공고 분석을 중단했습니다.");
}
analysisErrors.forEach((err) => {
if (err === "1") nextToastMessages.push("업로드에 실패했습니다.");
else if (err !== "not_saved") nextToastMessages.push(err);
});
if (!toastMessage) {
if (nextToastMessages.length === 0) {
return;
}
const openToastTimer = window.setTimeout(() => {
setJobPostingToastMessage(toastMessage);
setToastMessages(nextToastMessages);
window.history.replaceState(null, "", window.location.pathname);
}, 0);
const toastTimer = window.setTimeout(() => {
setJobPostingToastMessage(null);
}, 3000);
setToastMessages([]);
}, 4000);
return () => {
window.clearTimeout(openToastTimer);
window.clearTimeout(toastTimer);
};
}, []);
// ✅ 교체할 부분: URL에서 여러 개의 에러를 가져오도록 useEffect 수정
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
// getAll을 사용하여 배열 형태로 모두 가져옵니다
const analysisErrors = searchParams.getAll("analysisError");
const isCanceled = searchParams.get("analysisCanceled");
if (analysisErrors.includes("not_saved")) {
const modalTimer = window.setTimeout(() => {
setShowInvalidDataModal(true);
window.history.replaceState(null, "", window.location.pathname);
}, 0);
return () => window.clearTimeout(modalTimer);
}
const nextToastMessages: string[] = [];
if (isCanceled === "1") {
nextToastMessages.push("공고 분석을 중단했습니다.");
}
analysisErrors.forEach((err) => {
if (err === "1") nextToastMessages.push("업로드에 실패했습니다.");
else if (err !== "not_saved") nextToastMessages.push(err);
});
if (analysisErrors.length > 0 || isCanceled) {
window.history.replaceState(null, "", window.location.pathname);
}
if (nextToastMessages.length === 0) {
return;
}
const openToastTimer = window.setTimeout(() => {
setToastMessages(nextToastMessages);
}, 0);
const toastTimer = window.setTimeout(() => {
setToastMessages([]);
}, 4000);
return () => {
window.clearTimeout(openToastTimer);
window.clearTimeout(toastTimer);
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/job/create/page.tsx` around lines 115 - 159, Update
the useEffect handling URL query parameters in the create page so analysisError
and analysisCanceled are cleared with history.replaceState even when
nextToastMessages is empty. Perform the URL cleanup before the early return,
while preserving the existing modal handling and toast behavior.


const removeToast = (indexToRemove: number) => {
setToastMessages((prev) => prev.filter((_, i) => i !== indexToRemove));
};

const handleSubmit = () => {
saveJobPostingDraft({
files: attachedFiles,
Expand Down Expand Up @@ -172,44 +206,46 @@ export default function JobPostingCreatePage() {
/>

<div className="flex min-h-0 flex-1 flex-col items-center justify-center self-stretch px-2 pb-2">
<main className="flex h-full min-h-0 flex-col items-center justify-start self-stretch overflow-hidden rounded-card-l bg-fill-quaternary-assistive">
<div
<main className="relative flex h-full min-h-0 w-full flex-col items-center justify-center overflow-hidden rounded-card-l bg-fill-quaternary-assistive">
<section
ref={inputContainerRef}
className={clsx(
"flex flex-1 flex-col items-center self-stretch",
isInputActive
? "justify-center gap-0 -translate-y-[4dvh]"
: "justify-center gap-[6.6dvh] pb-[2dvh]",
"z-10 flex w-full flex-col items-center gap-[60px] transition-transform duration-500 ease-in-out",
isInputActive ? "translate-y-0" : "-translate-y-[140px]",
)}
>
<section className="flex flex-col items-center gap-[60px]">
<div className="flex flex-col items-center gap-2">
<h1 className="text-center text-h28-bold text-text-neutral-title [font-feature-settings:'liga'_off,'clig'_off]">
지원하고자하는 기업의 공고를 붙여넣으세요
</h1>
<p className="text-center text-b16-med text-text-neutral-description [font-feature-settings:'liga'_off,'clig'_off]">
첨부한 내용을 바탕으로 모의 공고가 생성되고 내가 작성한
자소서를 채점받을 수 있습니다.
</p>
</div>

<LLMInput
value={jobPostingInputValue}
onChange={setJobPostingInputValue}
onFilesChange={setAttachedFiles}
onFocus={() => setIsInputActive(true)}
onSubmit={handleSubmit}
defaultFiles={initialDraft.files}
/>
</section>

{!isInputActive && (
<section className="flex items-start gap-5">
{INTRO_STEPS.map((step) => (
<JobPostingStepCard key={step.step} step={step} />
))}
</section>
<div className="flex flex-col items-center gap-2">
<h1 className="text-center text-h28-bold text-text-neutral-title [font-feature-settings:'liga'_off,'clig'_off]">
지원하고자하는 기업의 공고를 붙여넣으세요
</h1>
<p className="text-center text-b16-med text-text-neutral-description [font-feature-settings:'liga'_off,'clig'_off]">
첨부한 내용을 바탕으로 모의 공고가 생성되고 내가 작성한 자소서를
채점받을 수 있습니다.
</p>
</div>

<LLMInput
value={jobPostingInputValue}
onChange={setJobPostingInputValue}
onFilesChange={setAttachedFiles}
onFocus={() => setIsInputActive(true)}
onSubmit={handleSubmit}
defaultFiles={initialDraft.files}
/>
</section>

<section
className={clsx(
"absolute left-1/2 top-1/2 z-0 flex w-max -translate-x-1/2 items-start gap-5 transition-all duration-350 ease-in-out",
isInputActive
? "pointer-events-none opacity-0 translate-y-20"
: "translate-y-10 opacity-100 ",
)}
</div>
>
{INTRO_STEPS.map((step) => (
<JobPostingStepCard key={step.step} step={step} />
))}
</section>
</main>
</div>

Expand Down Expand Up @@ -247,13 +283,18 @@ export default function JobPostingCreatePage() {
</div>
)}

{jobPostingToastMessage && (
<Toast
message={jobPostingToastMessage}
variant="warning"
onClose={() => setJobPostingToastMessage(null)}
className="!right-7 !bottom-7 !max-w-none !rounded-card"
/>
{toastMessages.length > 0 && (
<div className="fixed bottom-7 right-7 z-50 flex flex-col gap-3">
{toastMessages.map((msg, index) => (
<Toast
key={index}
message={msg}
variant="warning"
onClose={() => removeToast(index)}
className="!static !max-w-none !rounded-card"
/>
))}
</div>
Comment on lines +286 to +297

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Tailwind CSS v4 important modifier syntax leading exclamation mark supported

💡 Result:

In Tailwind CSS v4, the recommended syntax for the important modifier has changed from a leading exclamation mark (e.g.,!bg-red-500) to a trailing exclamation mark (e.g., bg-red-500!) [1][2][3]. While the leading exclamation mark syntax used in v3 remains supported for backwards compatibility, it is officially deprecated [2][3]. The trailing syntax is the new standard [1]. This change was implemented to provide more consistent placement of the exclamation mark, especially when combined with variants [1]. For example, in v3, a variant utility like hover:!bg-red-500 placed the modifier in the middle of the class name, whereas in v4, hover:bg-red-500! places it at the very end of the class name [1]. Official documentation and tooling, such as the official ESLint plugin, now recommend and enforce the trailing syntax [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo files likely relevant ==="
git ls-files | rg '(^|/)Toast\.tsx$|jobdri/src/app/mockApply/job/create/page\.tsx$|^(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$|tailwind\.config\.(js|ts|cjs|mjs)|postcss\.config\.(js|cjs|mjs|ts)$' || true

echo
echo "=== package tailwind versions ==="
if [ -f package.json ]; then cat package.json | jq -r '.dependencies.tailwind?:.devDependencies.tailwind?//empty, .dependencies."tailwindcss"?:.devDependencies."tailwindcss"?//empty' 2>/dev/null || true; fi
if [ -f pnpm-lock.yaml ]; then rg -n "tailwindcss|`@tailwindcss`" pnpm-lock.yaml | head -40 || true; fi
if [ -f package-lock.json ]; then cat package-lock.json | jq -r '.packages[""]?.dependencies?.["tailwindcss"], .packages[""]?.devDependencies?.["tailwindcss"], .packages[""]?.dependencies?.tailwind, .packages[""]?.devDependencies?.tailwind' 2>/dev/null || true; fi

echo
echo "=== Toast file ==="
toast_file="$(git ls-files | rg '(^|/)Toast\.tsx$' | head -1)"
if [ -n "${toast_file:-}" ]; then
  wc -l "$toast_file"
  sed -n '1,120p' "$toast_file"
fi

echo
echo "=== create page relevant lines ==="
page_file="jobdri/src/app/mockApply/job/create/page.tsx"
if [ -f "$page_file" ]; then
  wc -l "$page_file"
  sed -n '240,315p' "$page_file"
fi

echo
echo "=== removeToast/addToast usages in page ==="
rg -n "toastMessages|removeToast|addToast|useState|Array\.from\(" "$page_file" || true

echo
echo "=== Tailwind postcss tailwind config snippets ==="
for f in $(git ls-files | rg '(^|/)tailwind\.config\.(js|ts|cjs|jsx|tsx|mjs)$|(^|/)postcss\.config\.(js|cjs|mjs|ts)$|(^|/)package\.json$'); do
  echo "--- $f"
  sed -n '1,220p' "$f" | rg -n "tailwind|postcss|content|plugins|daisyui|v4|css" -C 2 || true
done

Repository: JobDri-Developer/FrontEnd

Length of output: 5768


🌐 Web query:

Tailwind v4 documentation important modifier deprecated leading exclamation mark still generated

💡 Result:

In Tailwind CSS v4, the use of a leading exclamation mark (e.g., !bg-red-500) to apply the !important modifier is officially deprecated [1][2]. While the leading exclamation mark syntax is still supported in v4 for backwards compatibility, the new, recommended standard is to use a trailing exclamation mark (e.g., bg-red-500!) [1][3][4]. Key details regarding this change: - Consistency: The postfix (!) syntax ensures that the important modifier is always at a consistent position at the end of the class name, regardless of whether variants (like hover: or dark:) are applied [3]. - Migration: It is recommended to migrate existing codebases to the new trailing syntax [3][2]. Tools such as ESLint plugins (e.g., eslint-plugin-tailwindcss) provide rules to identify and automatically fix the deprecated leading syntax [2]. - Usage: This applies to standard HTML class attributes as well as the @apply directive [1][5][4]. Source evidence confirming this includes the official Tailwind CSS upgrade guide [1] and developer discussions regarding v4 standards [3][4].

Citations:


🌐 Web query:

Tailwind v4 important modifier deprecated leading exclamation mark generates CSS still supported

💡 Result:

In Tailwind CSS v4, the important modifier syntax has been updated, but the legacy syntax remains supported for backward compatibility [1][2]. The current recommendation is to place the exclamation mark (!) at the end of the class name (e.g., bg-red-500!) rather than at the beginning (e.g.,!bg-red-500) [1][3]. This change was implemented to ensure consistent placement of the modifier, particularly when used with variants [3]. While the leading exclamation mark syntax (which was standard in v3) is officially deprecated, it is still supported by Tailwind CSS v4 to allow for easier transitions and compatibility with existing codebases [1][2][4].

Citations:


Use stable toast IDs and avoid index-based dismissals.

removeToast(index) together with key={index} can re-render the wrong rows when a non-last toast is dismissed. Store the toasts as { id: string; message: string } and filter by id on close.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/job/create/page.tsx` around lines 286 - 297, Update
the toast state and rendering in the job creation page to store each toast as an
object with a stable string id and message. In the toastMessages.map flow, use
the toast id for the React key and pass that id to removeToast; update
removeToast to filter by id rather than array index, while preserving the
existing warning styling and close behavior.

)}
</div>
);
Expand Down
121 changes: 111 additions & 10 deletions jobdri/src/app/mockApply/job/loading/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ const loadingStatusMessages = [

type LoadingStepStatus = "done" | "active" | "pending";

interface InvalidField {
message?: string;
[key: string]: unknown;
}

interface ParsedError {
message?: string;
invalidFields?: InvalidField[];
[key: string]: unknown;
}

function createRandomLoadingDurationMs() {
return (
MIN_LOADING_DURATION_MS +
Expand Down Expand Up @@ -134,39 +145,129 @@ export default function JobPostingLoadingPage() {
: undefined;

if (!rawText && !imageObjectKey) {
throw new Error("분석할 채용 공고가 없습니다.");
throw new Error(JSON.stringify(["분석할 채용 공고가 없습니다."]));
}

const accepted = await ingestJobPosting({ rawText, imageObjectKey });
const status = await waitForJobPostingIngest(accepted.taskId);

let status;
try {
status = await waitForJobPostingIngest(accepted.taskId);
} catch (err: unknown) {
if (err instanceof Error) {
try {
const parsedStatus = JSON.parse(err.message);
if (
parsedStatus &&
(parsedStatus.error ||
parsedStatus.failureReason ||
parsedStatus.status === "FAILED")
) {
status = parsedStatus;
} else {
throw err;
}
} catch {
throw err;
}
} else {
throw err;
}
}

if (
status &&
(status.status === "FAILED" || status.error || status.failureReason)
) {
const errorMessages: string[] = [];
const rawErrorData =
status.error || status.failureReason || status.message;

let parsedError: ParsedError | null = null;

if (rawErrorData) {
if (typeof rawErrorData === "object") {
parsedError = rawErrorData as ParsedError;
} else if (typeof rawErrorData === "string") {
try {
parsedError = JSON.parse(rawErrorData) as ParsedError;
} catch {
try {
const fixedJsonStr = rawErrorData.replace(/'/g, '"');
parsedError = JSON.parse(fixedJsonStr) as ParsedError;
} catch {
errorMessages.push(rawErrorData);
}
}
}
}
Comment on lines +188 to +203

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Single-quote→double-quote coercion can mangle user-facing messages.

Line 196 replaces every ' in the payload, so a Python-style payload whose values contain apostrophes parses into corrupted text that is then surfaced as a toast. Also, when JSON.parse succeeds but yields a non-object (e.g. "null", "42"), parsedError becomes truthy-checked at line 205 and the original rawErrorData is dropped in favor of the generic fallback.

🩹 Suggested guard
           } else if (typeof rawErrorData === "string") {
             try {
-              parsedError = JSON.parse(rawErrorData) as ParsedError;
+              const parsed: unknown = JSON.parse(rawErrorData);
+              if (parsed && typeof parsed === "object") {
+                parsedError = parsed as ParsedError;
+              } else {
+                errorMessages.push(rawErrorData);
+              }
             } catch {
-              try {
-                const fixedJsonStr = rawErrorData.replace(/'/g, '"');
-                parsedError = JSON.parse(fixedJsonStr) as ParsedError;
-              } catch {
-                errorMessages.push(rawErrorData);
-              }
+              errorMessages.push(rawErrorData);
             }
           }

If the backend really does emit Python-repr payloads, fix it at the source rather than with a quote swap here.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (rawErrorData) {
if (typeof rawErrorData === "object") {
parsedError = rawErrorData as ParsedError;
} else if (typeof rawErrorData === "string") {
try {
parsedError = JSON.parse(rawErrorData) as ParsedError;
} catch {
try {
const fixedJsonStr = rawErrorData.replace(/'/g, '"');
parsedError = JSON.parse(fixedJsonStr) as ParsedError;
} catch {
errorMessages.push(rawErrorData);
}
}
}
}
if (rawErrorData) {
if (typeof rawErrorData === "object") {
parsedError = rawErrorData as ParsedError;
} else if (typeof rawErrorData === "string") {
try {
const parsed: unknown = JSON.parse(rawErrorData);
if (parsed && typeof parsed === "object") {
parsedError = parsed as ParsedError;
} else {
errorMessages.push(rawErrorData);
}
} catch {
errorMessages.push(rawErrorData);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/job/loading/page.tsx` around lines 188 - 203, Update
the rawErrorData parsing in the error-handling block to avoid globally replacing
apostrophes, preserving user-facing message text; if Python-style payload
support is required, correct that representation at the backend source instead.
Validate JSON.parse results before assigning parsedError so only object-shaped
ParsedError values are accepted, while primitive or null results retain
rawErrorData for errorMessages or the existing fallback.


if (parsedError) {
if (
Array.isArray(parsedError.invalidFields) &&
parsedError.invalidFields.length > 0
) {
parsedError.invalidFields.forEach((field: InvalidField) => {
if (field.message) {
errorMessages.push(field.message);
}
});
}

if (errorMessages.length === 0 && parsedError.message) {
errorMessages.push(parsedError.message);
}
}

if (errorMessages.length === 0) {
errorMessages.push(
status.message || "채용 공고 분석에 실패했습니다.",
);
}

throw new Error(JSON.stringify(errorMessages));
}

if (!status.result) {
throw new Error("채용 공고 분석 결과를 확인할 수 없습니다.");
throw new Error(
JSON.stringify(["채용 공고 분석 결과를 확인할 수 없습니다."]),
);
}

saveJobPostingAnalysis(status.result);

const resultJobPostingId = status.result.saved?.jobPostingId;
const isSavedToDb = status.result.savedToDatabase ?? true;

// ID가 존재하고 DB 저장이 정상적으로 완료되었을 때만 리뷰 페이지로 이동
return resultJobPostingId && isSavedToDb
? `/mockApply/job/${resultJobPostingId}/review`
: "/mockApply/job/create?analysisError=not_saved";
};

if (!analysisPromiseRef.current) {
analysisPromiseRef.current = analyzeJobPosting().catch((error) => {
const message =
error instanceof Error
? error.message
: "채용 공고 분석에 실패했습니다.";
let params = "";
try {
const rawMessage = error instanceof Error ? error.message : "";

return `/mockApply/job/create?analysisError=${encodeURIComponent(message)}`;
if (rawMessage.startsWith("[") && rawMessage.endsWith("]")) {
const messages = JSON.parse(rawMessage) as string[];
if (messages.length > 0) {
params = messages
.map((msg) => `analysisError=${encodeURIComponent(msg)}`)
.join("&");
}
} else {
params = `analysisError=${encodeURIComponent(rawMessage || "채용 공고 분석에 실패했습니다.")}`;
}
} catch {
params = `analysisError=${encodeURIComponent("채용 공고 분석에 실패했습니다.")}`;
}
return `/mockApply/job/create?${params}`;
});
}

void analysisPromiseRef.current.then((destination) => {
void analysisPromiseRef.current?.then((destination) => {
if (isActive) {
router.replace(destination);
}
Expand Down
Loading