From 43e16861f3e60f9b62e76b1b47779be0ef575a77 Mon Sep 17 00:00:00 2001 From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:15:44 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=ED=81=B4=EB=A6=BD=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B5=EC=82=AC=20=EA=B3=B5=EC=9A=A9=20=ED=9B=85?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/upload/_step/UploadStepPage.tsx | 246 ++++------------------ src/shared/hooks/index.ts | 1 + src/shared/hooks/useClipboardCopy.ts | 64 ++++++ src/shared/index.ts | 1 + 4 files changed, 103 insertions(+), 209 deletions(-) create mode 100644 src/shared/hooks/index.ts create mode 100644 src/shared/hooks/useClipboardCopy.ts diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx index 50e3166..896f6b7 100644 --- a/src/pages/upload/_step/UploadStepPage.tsx +++ b/src/pages/upload/_step/UploadStepPage.tsx @@ -5,6 +5,7 @@ import { Box, Button, Flex, Text, Textarea, VStack } from "@chakra-ui/react"; import { type ProjectStepResponse, + PromptCard, ROADMAP_STEP_CODES, ROADMAP_STEP_NAMES, completeProjectAPI, @@ -12,56 +13,13 @@ import { startStepAPI, useGetProjectDetail, } from "@/entities"; -import { ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared"; import { - ArrowLeftIcon, - ArrowRightIcon, - CopyIcon, - DocumentTextIcon, -} from "@/shared/_assets/icons"; - -function PromptLine({ line }: { line: string }) { - if (line.startsWith("# ") || line === "#") { - return ( - - {line} - - ); - } - if (line.startsWith("//")) { - return ( - - {line} - - ); - } - return ( - - {line} - - ); -} + ROUTE_PATHS, + getApiErrorMessage, + toaster, + useClipboardCopy, +} from "@/shared"; +import { ArrowLeftIcon, ArrowRightIcon } from "@/shared/_assets/icons"; function StepIndicator({ current, @@ -160,8 +118,8 @@ function UploadStepContent({ const navigate = useNavigate(); const [resultText, setResultText] = useState(""); - const [copiedPrompt, setCopiedPrompt] = useState(false); - const [copiedFormat, setCopiedFormat] = useState(false); + const { copied: copiedPrompt, copy: copyPrompt } = useClipboardCopy(); + const { copied: copiedFormat, copy: copyFormat } = useClipboardCopy(); const [stepData, setStepData] = useState(null); const [isStepLoading, setIsStepLoading] = useState(false); @@ -195,31 +153,6 @@ function UploadStepContent({ fetchStep(); }, [projectId, stepCode]); - const copyToClipboard = (text: string, setter: (v: boolean) => void) => { - if (!navigator?.clipboard?.writeText) { - toaster.create({ - type: "error", - description: - "클립보드 복사에 실패했습니다. 브라우저가 클립보드를 지원하지 않습니다.", - }); - return; - } - navigator.clipboard - .writeText(text) - .then(() => { - setter(true); - setTimeout(() => setter(false), 2000); - }) - .catch((error) => { - console.error("Failed to copy to clipboard", error); - toaster.create({ - type: "error", - description: - "클립보드 복사에 실패했습니다. 브라우저 권한 또는 HTTPS 환경을 확인해주세요.", - }); - }); - }; - const goToPrevStep = () => { if (stepNum <= 1) { navigate(ROUTE_PATHS.FILE_UPLOAD); @@ -345,67 +278,16 @@ function UploadStepContent({ - - - - - - 생성된 프롬프트 - - - - - stepData?.providedPromptSnapshot && - copyToClipboard( - stepData.providedPromptSnapshot, - setCopiedPrompt, - ) - } - px="13px" - py="7px" - _hover={{ boxShadow: "0px 2px 4px 0px rgba(0,0,0,0.08)" }} - > - - - - {copiedPrompt ? "복사됨 ✓" : "복사하기"} - - - - - - - {isStepLoading ? ( + 프롬프트를 불러오는 중... - ) : ( - stepData?.providedPromptSnapshot - ?.split("\n") - .map((line, i) => ) - )} + - + ) : stepData?.providedPromptSnapshot ? ( + { + void copyPrompt(stepData.providedPromptSnapshot); + }} + /> + ) : null} {stepData?.formatPrompt && ( @@ -435,73 +322,14 @@ function UploadStepContent({ 이 프롬프트를 사용하여 ai와 함께 작업한 결과를 추출해주세요. - - - - - - 작업 결과 추출 프롬프트 - - - - - copyToClipboard(stepData.formatPrompt, setCopiedFormat) - } - px="13px" - py="7px" - _hover={{ - boxShadow: "0px 2px 4px 0px rgba(0,0,0,0.08)", - }} - > - - - - {copiedFormat ? "복사됨 ✓" : "복사하기"} - - - - - - - {stepData.formatPrompt.split("\n").map((line, i) => ( - - ))} - - + { + void copyFormat(stepData.formatPrompt); + }} + /> )} diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts new file mode 100644 index 0000000..ea68209 --- /dev/null +++ b/src/shared/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useClipboardCopy"; diff --git a/src/shared/hooks/useClipboardCopy.ts b/src/shared/hooks/useClipboardCopy.ts new file mode 100644 index 0000000..42c6f84 --- /dev/null +++ b/src/shared/hooks/useClipboardCopy.ts @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import { toaster } from "../utils"; + +type UseClipboardCopyOptions = { + successDuration?: number; + unsupportedMessage?: string; + errorMessage?: string; +}; + +export const useClipboardCopy = ({ + successDuration = 2000, + unsupportedMessage, + errorMessage, +}: UseClipboardCopyOptions = {}) => { + const [copied, setCopied] = useState(false); + const resetTimerRef = useRef(null); + + const clearResetTimer = useCallback(() => { + if (resetTimerRef.current !== null) { + window.clearTimeout(resetTimerRef.current); + resetTimerRef.current = null; + } + }, []); + + useEffect(() => clearResetTimer, [clearResetTimer]); + + const copy = useCallback( + async (text: string) => { + if (!navigator?.clipboard?.writeText) { + toaster.create({ + type: "error", + description: + unsupportedMessage ?? + "클립보드 복사에 실패했습니다. 브라우저가 클립보드를 지원하지 않습니다.", + }); + return false; + } + + try { + await navigator.clipboard.writeText(text); + setCopied(true); + clearResetTimer(); + resetTimerRef.current = window.setTimeout(() => { + setCopied(false); + resetTimerRef.current = null; + }, successDuration); + return true; + } catch (error) { + console.error("Failed to copy to clipboard", error); + toaster.create({ + type: "error", + description: + errorMessage ?? + "클립보드 복사에 실패했습니다. 브라우저 권한 또는 HTTPS 환경을 확인해주세요.", + }); + return false; + } + }, + [clearResetTimer, errorMessage, successDuration, unsupportedMessage], + ); + + return { copied, copy }; +}; diff --git a/src/shared/index.ts b/src/shared/index.ts index a6f2226..1762bff 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,6 +1,7 @@ export * from "./_assets"; export * from "./components"; export * from "./constants"; +export * from "./hooks"; export * from "./libs"; export * from "./supabase"; export * from "./theme"; From 7102da917ffe76706a5d47483384d8c4e52b035c Mon Sep 17 00:00:00 2001 From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:31:39 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=EB=8B=A8=EA=B3=84=20Indicator,?= =?UTF-8?q?=20=EC=9E=91=EC=97=85=20=EA=B2=B0=EA=B3=BC=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20Input=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/upload/index.ts | 1 + .../upload/ui/UploadStepIndicator.tsx | 89 +++++++++++ .../upload/ui/UploadStepResultInput.tsx | 57 +++++++ src/features/upload/ui/index.ts | 2 + src/pages/upload/_step/UploadStepPage.tsx | 143 +----------------- 5 files changed, 156 insertions(+), 136 deletions(-) create mode 100644 src/features/upload/ui/UploadStepIndicator.tsx create mode 100644 src/features/upload/ui/UploadStepResultInput.tsx create mode 100644 src/features/upload/ui/index.ts diff --git a/src/features/upload/index.ts b/src/features/upload/index.ts index 178cd64..70c139d 100644 --- a/src/features/upload/index.ts +++ b/src/features/upload/index.ts @@ -1 +1,2 @@ +export * from "./ui"; export * from "./utils"; diff --git a/src/features/upload/ui/UploadStepIndicator.tsx b/src/features/upload/ui/UploadStepIndicator.tsx new file mode 100644 index 0000000..308ec3c --- /dev/null +++ b/src/features/upload/ui/UploadStepIndicator.tsx @@ -0,0 +1,89 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; + +import { ROADMAP_STEP_NAMES } from "@/entities"; + +type Props = { + current: number; + stepCodes: string[]; +}; + +export const UploadStepIndicator = ({ current, stepCodes }: Props) => { + return ( + + + {Array.from({ length: stepCodes.length - 1 }, (_, i) => ( + + ))} + + + + {stepCodes.map((code, i) => { + const stepId = i + 1; + const isActive = stepId === current; + const isDone = stepId < current; + + return ( + + + + + + {stepId} + + + + + + {ROADMAP_STEP_NAMES[code] ?? code} + + + ); + })} + + + ); +}; diff --git a/src/features/upload/ui/UploadStepResultInput.tsx b/src/features/upload/ui/UploadStepResultInput.tsx new file mode 100644 index 0000000..0fbbe55 --- /dev/null +++ b/src/features/upload/ui/UploadStepResultInput.tsx @@ -0,0 +1,57 @@ +import { Box, Text, Textarea, VStack } from "@chakra-ui/react"; + +type Props = { + value: string; + onChange: (value: string) => void; +}; + +export const UploadStepResultInput = ({ value, onChange }: Props) => { + return ( + + + 작업 결과 입력 + + + +