From 41a8de687605abe25cb51fc81c179003cdd646b9 Mon Sep 17 00:00:00 2001 From: girimNam Date: Sun, 5 Jul 2026 01:37:38 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=84=A0=ED=96=89=EA=B8=B0?= =?UTF-8?q?=EC=88=A0=20=ED=83=90=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9D=BC=EB=B6=80=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/layout.tsx | 2 +- src/app/(main)/search/page.tsx | 46 ++++++++++++++++++++++-- src/app/globals.css | 2 ++ src/components/search/SearchTitle.tsx | 20 +++++++++++ src/components/ui/AI_Creation_Button.tsx | 24 +++++++++++++ src/components/ui/TextArea.tsx | 42 ++++++++++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/components/search/SearchTitle.tsx create mode 100644 src/components/ui/AI_Creation_Button.tsx create mode 100644 src/components/ui/TextArea.tsx diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 3b9122d..2c767c3 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -13,7 +13,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
-
{children}
+
{children}
); diff --git a/src/app/(main)/search/page.tsx b/src/app/(main)/search/page.tsx index 54b6578..49fffa1 100644 --- a/src/app/(main)/search/page.tsx +++ b/src/app/(main)/search/page.tsx @@ -1,7 +1,49 @@ +import Title from "@/components/search/SearchTitle"; +import { TextField } from "@/components/ui/TextField"; +import { TextArea } from "@/components/ui/TextArea"; +import { AICreationButton } from "@/components/ui/AI_Creation_Button"; + export default function SearchPage() { return ( - <> +
+
+ + + <div className="flex flex-col gap-4"> + <TextField label="발명의 명칭" placeholder="EX) 생분해성 고분자 코팅 조성물" /> + <div className="flex flex-row gap-3"> + <TextField label="기술 분야" placeholder="EX) 고분자 화학 코팅" /> + <TextField label="IPC 분류" placeholder="EX) C09D 5/00" /> + </div> + <TextArea + label="핵심 기술 설명" + placeholder="발명의 핵심 구성과 작동 방식을 간단히 설명해주세요" + rows={4} + /> + </div> + </div> + + <div className="flex flex-col gap-6"> + <Title stepnum={2} title="출원인 정보" /> + + <div className="flex flex-row gap-3"> + <TextField label="사명 (선택)" placeholder="EX) 그린폴리머(주)" /> + <TextField label="의뢰인 (선택)" placeholder="담당자 또는 발명자 성명을 입력해주세요" /> + </div> + </div> + + <div className="flex flex-col gap-6"> + <div className="flex flex-row justify-between"> + <Title + stepnum={3} + title="구성요소 분석" + label="청구항을 구성요소 단위로 분해해 판단의 정밀성을 높힙니다." + /> + <AICreationButton /> + </div> + </div> + <div></div> - </> + </div> ); } diff --git a/src/app/globals.css b/src/app/globals.css index 4422335..7f6bf36 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -68,6 +68,8 @@ --color-icon-neutral-subtle: #909ba6; /* Gray-50 */ --color-icon-primary-default: #4478F0; + --color-icon-primary-emphasize: #2152E5; + /* Stroke */ --color-stroke-divider: #f2f6f9; /* Gray-90 */ diff --git a/src/components/search/SearchTitle.tsx b/src/components/search/SearchTitle.tsx new file mode 100644 index 0000000..ae293f3 --- /dev/null +++ b/src/components/search/SearchTitle.tsx @@ -0,0 +1,20 @@ +type TitleProps = { + stepnum: number; + title: string; + label?: string; +}; + +export default function Title({ stepnum, title, label }: TitleProps) { + return ( + <div className="flex flex-row gap-3 items-center"> + <div className="w-6 h-6 flex items-center justify-center rounded-sm bg-bg-primary-hover text-label-emphasis-15 text-inverse-on-primary"> + {stepnum} + </div> + + <div className="flex flex-row gap-2 items-center"> + <p className="text-title-primary text-body-emphasis-19">{title}</p> + <p className="text-caption-label text-label-13">{label}</p> + </div> + </div> + ); +} diff --git a/src/components/ui/AI_Creation_Button.tsx b/src/components/ui/AI_Creation_Button.tsx new file mode 100644 index 0000000..6fcb045 --- /dev/null +++ b/src/components/ui/AI_Creation_Button.tsx @@ -0,0 +1,24 @@ +import { forwardRef } from "react"; +import { cn } from "@/lib/cn"; +import AICreationIcon from "@/components/icons/icon-aiCreation.svg"; + +export type AICreationButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>; + +export const AICreationButton = forwardRef<HTMLButtonElement, AICreationButtonProps>( + ({ className, type = "button", ...props }, ref) => ( + <button + ref={ref} + type={type} + className={cn( + "inline-flex cursor-pointer items-center gap-1 rounded-sm border border-stroke-primary bg-bg-surface py-2 pl-2 pr-3 text-label-15 text-primary-default transition-colors hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle", + className + )} + {...props} + > + <AICreationIcon className="h-5 w-5 [&_path]:fill-icon-primary-emphasize" aria-hidden /> + AI 자동 생성 + </button> + ) +); + +AICreationButton.displayName = "AICreationButton"; diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx new file mode 100644 index 0000000..ebe20ab --- /dev/null +++ b/src/components/ui/TextArea.tsx @@ -0,0 +1,42 @@ +import { useId } from "react"; +import { cn } from "@/lib/cn"; + +type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & { + label: string; + error?: string; + ref?: React.Ref<HTMLTextAreaElement>; +}; + +export function TextArea({ label, error, className, id, ref, ...props }: TextAreaProps) { + const autoId = useId(); + const textareaId = id ?? autoId; + const errorId = `${textareaId}-error`; + + return ( + <div className="flex w-full flex-col items-start gap-1"> + <label + htmlFor={textareaId} + className="flex h-5 items-center text-label-13 text-title-secondary" + > + {label} + </label> + <textarea + ref={ref} + id={textareaId} + aria-invalid={!!error} + aria-describedby={error ? errorId : undefined} + className={cn( + "h-27.5 w-full resize-none rounded-lg border border-outline-default bg-bg-surface p-4 text-body-17 text-body-primary placeholder:text-caption-label focus:border-stroke-primary focus:outline-none", + error && "border-stroke-error focus:border-stroke-error", + className + )} + {...props} + /> + {error && ( + <p id={errorId} className="flex h-5 items-center text-label-13 text-error-default"> + {error} + </p> + )} + </div> + ); +} From 5a0414260cfb42fbaca060621d8bbdeed6ce7226 Mon Sep 17 00:00:00 2001 From: girimNam <rlfla@naver.com> Date: Wed, 8 Jul 2026 11:03:06 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B5=AC=EC=84=B1=EC=9A=94?= =?UTF-8?q?=EC=86=8C=20=EB=B6=84=ED=95=B4=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 1 + .../search/ElementList/ElementList.tsx | 64 +++++++++++++++++++ .../search/ElementList/ElementRow.tsx | 42 ++++++++++++ .../search/ElementList/ElementTitle.tsx | 23 +++++++ .../search/ElementList/LoadingIndicator.tsx | 12 ++++ 5 files changed, 142 insertions(+) create mode 100644 src/components/search/ElementList/ElementList.tsx create mode 100644 src/components/search/ElementList/ElementRow.tsx create mode 100644 src/components/search/ElementList/ElementTitle.tsx create mode 100644 src/components/search/ElementList/LoadingIndicator.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 7f6bf36..c7d9617 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -42,6 +42,7 @@ /* Primary */ --color-bg-primary-tint: #f5f9ff; /* Primary-120 */ --color-bg-primary-light: #eef5ff; /* Primary-110 */ + --color-bg-elementlist: #E4EFFF; /* Primary-100 */ /* Primary-light */ --color-bg-primary: #4478f0; /* Primary-50 */ diff --git a/src/components/search/ElementList/ElementList.tsx b/src/components/search/ElementList/ElementList.tsx new file mode 100644 index 0000000..172b744 --- /dev/null +++ b/src/components/search/ElementList/ElementList.tsx @@ -0,0 +1,64 @@ +"use client"; + +import PlusIcon from "@/components/icons/icon-plus.svg"; +import { LoadingIndicator } from "./LoadingIndicator"; +import { ElementRow } from "./ElementRow"; + +export interface Element { + id: string; + name: string; + description: string; +} + +interface ElementListProps { + elements: Element[]; + isLoading?: boolean; + onAdd: () => void; + onDelete: (id: string) => void; + onChange: (id: string, field: "name" | "description", value: string) => void; +} + +export function ElementList({ + elements, + isLoading = false, + onAdd, + onDelete, + onChange, +}: ElementListProps) { + return ( + <div className="w-full"> + <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-4 bg-bg-neutral-hover px-4 py-2.5"> + <span className="text-label-15 text-body-disabled">발명 구성요소</span> + <span className="text-label-15 text-body-disabled">설명</span> + <div className="w-11" /> + </div> + + {isLoading ? ( + <LoadingIndicator label="AI가 발명 정보를 분석해 구성요소를 추출하고 있습니다..." /> + ) : ( + <> + {elements.map((el, index) => ( + <ElementRow + key={el.id} + index={index} + name={el.name} + description={el.description} + onDelete={() => onDelete(el.id)} + onChangeName={(value) => onChange(el.id, "name", value)} + onChangeDescription={(value) => onChange(el.id, "description", value)} + /> + ))} + + <button + type="button" + onClick={onAdd} + className="flex items-center gap-1 px-4 py-3 text-label-17 text-primary-default transition-opacity cursor-pointer" + > + <PlusIcon className="h-5 w-5 text-icon-primary-emphasize [&_path]:fill-current" /> + 구성요소 추가 + </button> + </> + )} + </div> + ); +} diff --git a/src/components/search/ElementList/ElementRow.tsx b/src/components/search/ElementList/ElementRow.tsx new file mode 100644 index 0000000..c417752 --- /dev/null +++ b/src/components/search/ElementList/ElementRow.tsx @@ -0,0 +1,42 @@ +import TrashIcon from "@/components/icons/icon-trashcan.svg"; +import { ElementTitle } from "./ElementTitle"; + +interface ElementRowProps { + index: number; + name: string; + description: string; + onDelete: () => void; + onChangeName: (value: string) => void; + onChangeDescription: (value: string) => void; +} + +export function ElementRow({ + index, + name, + description, + onDelete, + onChangeName, + onChangeDescription, +}: ElementRowProps) { + return ( + <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-4 px-4 py-3"> + <ElementTitle index={index} value={name} onChange={onChangeName} /> + + <input + type="text" + value={description} + onChange={(e) => onChangeDescription(e.target.value)} + placeholder="구성요소 설명" + className="w-full bg-transparent text-label-17 text-body-secondary placeholder:text-caption-labe placeholder:text-label-17 focus:outline-none" + /> + + <button + type="button" + onClick={onDelete} + className="text-icon-neutral-subtle transition-colors p-2.5 hover:text-error-default" + > + <TrashIcon className="h-6 w-6 [&_path]:fill-current" /> + </button> + </div> + ); +} diff --git a/src/components/search/ElementList/ElementTitle.tsx b/src/components/search/ElementList/ElementTitle.tsx new file mode 100644 index 0000000..e845874 --- /dev/null +++ b/src/components/search/ElementList/ElementTitle.tsx @@ -0,0 +1,23 @@ +interface ElementTitleProps { + index: number; + value: string; + onChange: (value: string) => void; +} + +export function ElementTitle({ index, value, onChange }: ElementTitleProps) { + return ( + <div className="flex min-w-0 w-full items-center gap-3"> + <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-bg-elementlist text-label-emphasis-15 text-primary-sub"> + {String.fromCharCode(65 + index)} + </span> + + <input + type="text" + value={value} + onChange={(e) => onChange(e.target.value)} + placeholder="구성요소 명칭" + className="w-full bg-transparent text-title-secondary text-label-emphasis-17 placeholder:text-caption-label placeholder:text-label-emphasis-17 focus:outline-none" + /> + </div> + ); +} diff --git a/src/components/search/ElementList/LoadingIndicator.tsx b/src/components/search/ElementList/LoadingIndicator.tsx new file mode 100644 index 0000000..ad4cf15 --- /dev/null +++ b/src/components/search/ElementList/LoadingIndicator.tsx @@ -0,0 +1,12 @@ +type LoadingIndicatorProps = { + label: string; +}; + +export function LoadingIndicator({ label }: LoadingIndicatorProps) { + return ( + <div className="flex flex-col items-center justify-center gap-4 py-12"> + <div className="h-9 w-9 animate-spin rounded-full border-3 border-icon-primary-default border-t-transparent" /> + <span className="text-label-17 text-caption-label">{label}</span> + </div> + ); +} From 7c34002f59c89b1dad80e20a9c2e181b586dcd8a Mon Sep 17 00:00:00 2001 From: girimNam <rlfla@naver.com> Date: Wed, 8 Jul 2026 11:42:19 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=8F=AC?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=20=ED=8F=AC=EC=A7=80=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 2 +- src/components/search/Checklist.tsx | 32 ++++++++++++++++++++++++++ src/components/searchlist/Checkbox.tsx | 6 ++--- src/components/sidebar/Sidebar.tsx | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/components/search/Checklist.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5c3d1c7..6423be9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -88,7 +88,7 @@ export default function RootLayout({ }>) { return ( <html lang="ko" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}> - <body className="min-h-full flex flex-col">{children}</body> + <body className="h-full overflow-hidden">{children}</body> </html> ); } diff --git a/src/components/search/Checklist.tsx b/src/components/search/Checklist.tsx new file mode 100644 index 0000000..3640a05 --- /dev/null +++ b/src/components/search/Checklist.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useState } from "react"; +import { Checkbox } from "@/components/searchlist/Checkbox"; + +const ITEMS = ["미공개 기술", "관련 수치데이터 보유", "누락 정보 없음"] as const; + +export function Checklist() { + const [checked, setChecked] = useState<Record<string, boolean>>({}); + + return ( + <div className="flex items-center gap-8"> + {ITEMS.map((label, index) => { + const uniqueId = `checklist-${index}`; + + return ( + <div key={label} className="flex items-center gap-1"> + <Checkbox + id={uniqueId} + checked={!!checked[label]} + onChange={(e) => setChecked((prev) => ({ ...prev, [label]: e.target.checked }))} + /> + + <label htmlFor={uniqueId} className="cursor-pointer text-body-15 text-title-secondary"> + {label} + </label> + </div> + ); + })} + </div> + ); +} diff --git a/src/components/searchlist/Checkbox.tsx b/src/components/searchlist/Checkbox.tsx index 0cb9db5..0460b12 100644 --- a/src/components/searchlist/Checkbox.tsx +++ b/src/components/searchlist/Checkbox.tsx @@ -30,7 +30,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( return ( <label className={cn( - "inline-flex h-6 w-6 items-center justify-center", + "relative inline-flex h-6 w-6 items-center justify-center", disabled ? "cursor-not-allowed" : "cursor-pointer", className )} @@ -50,9 +50,9 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( /> <span className={cn( - "flex h-4 w-4 items-center justify-center rounded-[1px] border-2", + "flex h-4 w-4 items-center justify-center rounded-[1px] border-[1.5px]", isFilled - ? "border-primary-default bg-primary-default" + ? "border-icon-primary-default bg-icon-primary-default rounded-[1px]" : "border-outline-selected bg-bg-surface" )} > diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 1a30df4..4e579b7 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -19,7 +19,7 @@ export function Sidebar({ open, onToggle }: SidebarProps) { return ( <aside - className={`flex flex-col gap-5 px-3 py-5 border-r border-outline-sub transition-all duration-500 ${ + className={`flex h-full flex-col gap-5 border-r border-outline-sub px-3 py-5 transition-all duration-500 ${ open ? "w-72" : "w-15" }`} > From b76e7e9f590f5ed68da5902b3e9b6528c5b60c56 Mon Sep 17 00:00:00 2001 From: girimNam <rlfla@naver.com> Date: Wed, 8 Jul 2026 13:57:57 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20footer=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/search/page.tsx | 97 +++++++++++++++++-- .../search/ElementList/ElementList.tsx | 4 +- .../search/ElementList/ElementRow.tsx | 14 ++- src/components/search/Footer.tsx | 18 ++++ src/components/search/PatentImportModal.tsx | 49 ++++++++++ src/components/search/ResultCountButton.tsx | 33 +++++++ 6 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/components/search/Footer.tsx create mode 100644 src/components/search/PatentImportModal.tsx create mode 100644 src/components/search/ResultCountButton.tsx diff --git a/src/app/(main)/search/page.tsx b/src/app/(main)/search/page.tsx index 49fffa1..c05a5f1 100644 --- a/src/app/(main)/search/page.tsx +++ b/src/app/(main)/search/page.tsx @@ -1,13 +1,73 @@ +"use client"; + +import { useState } from "react"; import Title from "@/components/search/SearchTitle"; import { TextField } from "@/components/ui/TextField"; import { TextArea } from "@/components/ui/TextArea"; import { AICreationButton } from "@/components/ui/AI_Creation_Button"; +import { ElementList, type Element } from "@/components/search/ElementList/ElementList"; +import { Checklist } from "@/components/search/Checklist"; +import { Footer } from "@/components/search/Footer"; +import { PatentImportModal } from "@/components/search/PatentImportModal"; +import { Button } from "@/components/ui/Button"; export default function SearchPage() { + const [elements, setElements] = useState<Element[]>([ + { id: crypto.randomUUID(), name: "", description: "" }, + ]); + const [isLoading, setIsLoading] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleAdd = () => { + setElements((prev) => [...prev, { id: crypto.randomUUID(), name: "", description: "" }]); + }; + + const handleDelete = (id: string) => { + setElements((prev) => prev.filter((el) => el.id !== id)); + }; + + const handleChange = (id: string, field: "name" | "description", value: string) => { + setElements((prev) => prev.map((el) => (el.id === id ? { ...el, [field]: value } : el))); + }; + + const handleAICreate = () => { + setIsLoading(true); + setTimeout(() => { + setElements([ + { + id: crypto.randomUUID(), + name: "생분해성 베이스 수지", + description: "PLA·PBAT 블렌드 폴리에스터", + }, + { + id: crypto.randomUUID(), + name: "표면개질 나노 충전제", + description: "실란 처리된 무기 나노입자", + }, + { + id: crypto.randomUUID(), + name: "무용제 수계 분산 공정", + description: "유기용제 없이 수계 분산으로 코팅층 형성", + }, + { + id: crypto.randomUUID(), + name: "UV 경화 기교", + description: "자외선 경화형 가교제에 의한 표면 가교", + }, + ]); + setIsLoading(false); + }, 2000); + }; + return ( - <div className="flex flex-col gap-15 px-20 pb-5"> - <div className="flex flex-col gap-6"> - <Title stepnum={1} title="발명 정보" /> + <div className="flex flex-col gap-13 px-20 pb-5"> + <div className="flex flex-col gap-5"> + <div className="flex justify-between"> + <Title stepnum={1} title="발명 정보" /> + <Button variant="secondary" size="sm" onClick={() => setIsModalOpen(true)}> + 특허 번호로 불러오기 + </Button> + </div> <div className="flex flex-col gap-4"> <TextField label="발명의 명칭" placeholder="EX) 생분해성 고분자 코팅 조성물" /> @@ -23,27 +83,46 @@ export default function SearchPage() { </div> </div> - <div className="flex flex-col gap-6"> + <div className="flex flex-col gap-5"> <Title stepnum={2} title="출원인 정보" /> - <div className="flex flex-row gap-3"> <TextField label="사명 (선택)" placeholder="EX) 그린폴리머(주)" /> <TextField label="의뢰인 (선택)" placeholder="담당자 또는 발명자 성명을 입력해주세요" /> </div> </div> - <div className="flex flex-col gap-6"> - <div className="flex flex-row justify-between"> + <div className="flex flex-col gap-5"> + <div className="flex flex-row items-center justify-between"> <Title stepnum={3} title="구성요소 분석" label="청구항을 구성요소 단위로 분해해 판단의 정밀성을 높힙니다." /> - <AICreationButton /> + <AICreationButton onClick={handleAICreate} /> + </div> + + <div className="overflow-hidden border-y border-outline-default"> + <ElementList + elements={elements} + isLoading={isLoading} + onAdd={handleAdd} + onDelete={handleDelete} + onChange={handleChange} + /> </div> + + <Checklist /> </div> - <div></div> + <Footer /> + + {isModalOpen && ( + <PatentImportModal + initialPatentNumber="" + onClose={() => setIsModalOpen(false)} + onSubmit={() => setIsModalOpen(false)} + /> + )} </div> ); } diff --git a/src/components/search/ElementList/ElementList.tsx b/src/components/search/ElementList/ElementList.tsx index 172b744..dd6a478 100644 --- a/src/components/search/ElementList/ElementList.tsx +++ b/src/components/search/ElementList/ElementList.tsx @@ -52,9 +52,9 @@ export function ElementList({ <button type="button" onClick={onAdd} - className="flex items-center gap-1 px-4 py-3 text-label-17 text-primary-default transition-opacity cursor-pointer" + className="flex w-full items-center gap-1 px-4 py-3 text-label-17 text-primary-default transition-opacity cursor-pointer bg-bg-surface hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle" > - <PlusIcon className="h-5 w-5 text-icon-primary-emphasize [&_path]:fill-current" /> + <PlusIcon className=" m-2 h-5 w-5 text-icon-primary-emphasize [&_path]:fill-current" /> 구성요소 추가 </button> </> diff --git a/src/components/search/ElementList/ElementRow.tsx b/src/components/search/ElementList/ElementRow.tsx index c417752..df1b33f 100644 --- a/src/components/search/ElementList/ElementRow.tsx +++ b/src/components/search/ElementList/ElementRow.tsx @@ -22,18 +22,22 @@ export function ElementRow({ <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-4 px-4 py-3"> <ElementTitle index={index} value={name} onChange={onChangeName} /> - <input - type="text" + <textarea value={description} - onChange={(e) => onChangeDescription(e.target.value)} + onChange={(e) => { + onChangeDescription(e.target.value); + e.target.style.height = "auto"; + e.target.style.height = `${e.target.scrollHeight}px`; + }} placeholder="구성요소 설명" - className="w-full bg-transparent text-label-17 text-body-secondary placeholder:text-caption-labe placeholder:text-label-17 focus:outline-none" + rows={1} + className="w-full resize-none overflow-hidden bg-transparent text-label-17 text-body-secondary placeholder:text-caption-label placeholder:text-label-17 focus:outline-none" /> <button type="button" onClick={onDelete} - className="text-icon-neutral-subtle transition-colors p-2.5 hover:text-error-default" + className="p-2.5 text-icon-neutral-subtle transition-colors hover:text-error-default" > <TrashIcon className="h-6 w-6 [&_path]:fill-current" /> </button> diff --git a/src/components/search/Footer.tsx b/src/components/search/Footer.tsx new file mode 100644 index 0000000..a088402 --- /dev/null +++ b/src/components/search/Footer.tsx @@ -0,0 +1,18 @@ +import { Button } from "@/components/ui/Button"; +import { ResultCountButton } from "./ResultCountButton"; + +export function Footer() { + return ( + <div className="flex items-center justify-between"> + <div className="flex items-center gap-6 text-label-17 text-title-secondary"> + <span>탐색 결과 개수</span> + <ResultCountButton /> + </div> + + <Button size="sm" disabled> + 탐색 시작 + </Button> + {/* api 연동 이후 disabled 해제 관련 조건 추가 예정 */} + </div> + ); +} diff --git a/src/components/search/PatentImportModal.tsx b/src/components/search/PatentImportModal.tsx new file mode 100644 index 0000000..8042940 --- /dev/null +++ b/src/components/search/PatentImportModal.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useState } from "react"; +import CancelIcon from "@/components/icons/icon-cancel.svg"; +import { TextField } from "@/components/ui/TextField"; +import { Button } from "@/components/ui/Button"; + +interface PatentImportModalProps { + initialPatentNumber: string; + onClose: () => void; + onSubmit: (data: { patentNumber: string }) => void; +} + +export function PatentImportModal({ onClose, onSubmit }: PatentImportModalProps) { + const [patentNumber, setPatentNumber] = useState(""); + + return ( + <div + className="fixed inset-0 z-50 flex items-center justify-center bg-scrim-2" + onClick={onClose} + > + <div + className="w-138.5 p-6 flex flex-col gap-8 rounded-lg bg-bg-surface shadow-[0px_1px_6px_0px_rgba(144,155,165,0.36)]" + onClick={(e) => e.stopPropagation()} + > + <div className="flex items-center justify-between"> + <h2 className="text-title-emphasis-17 text-title-primary">특허번호로 기술 불러오기</h2> + <button + type="button" + onClick={onClose} + aria-label="닫기" + className="flex items-center justify-center cursor-pointer" + > + <CancelIcon className="h-6 w-6" aria-hidden /> + </button> + </div> + + <TextField + label="특허번호" + value={patentNumber} + onChange={(e) => setPatentNumber(e.target.value)} + placeholder="특허번호를 입력해주세요" + /> + + <Button onClick={() => onSubmit({ patentNumber })}>기술 불러오기</Button> + </div> + </div> + ); +} diff --git a/src/components/search/ResultCountButton.tsx b/src/components/search/ResultCountButton.tsx new file mode 100644 index 0000000..259369b --- /dev/null +++ b/src/components/search/ResultCountButton.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useState } from "react"; +import MinusIcon from "@/components/icons/icon-minus.svg"; +import PlusIcon from "@/components/icons/icon-plus.svg"; + +export function ResultCountButton() { + const [count, setCount] = useState(10); + + return ( + <div className="flex items-center overflow-hidden rounded-sm bg-bg-surface border border-outline-sub text-icon-neutral-subtle"> + <button + type="button" + onClick={() => setCount((n) => Math.max(1, n - 1))} + className="flex h-12 w-11 px-3 py-2 items-center justify-center transition-colors hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle" + > + <MinusIcon className="h-5 w-5 [&_path]:fill-current" /> + </button> + + <span className="flex w-16.5 h-12 py-2 items-center justify-center border-x border-outline-sub text-label-emphasis-17 text-title-primary"> + {count}개 + </span> + + <button + type="button" + onClick={() => setCount((n) => n + 1)} + className="flex h-12 w-11 px-3 py-2 items-center justify-center transition-colors hover:bg-bg-neutral-hover active:bg-bg-neutral-subtle" + > + <PlusIcon className="h-5 w-5 [&_path]:fill-current" /> + </button> + </div> + ); +} From 0bbe416bd8beed98417a81998a88b1c5e46c6746 Mon Sep 17 00:00:00 2001 From: girimNam <rlfla@naver.com> Date: Wed, 8 Jul 2026 14:58:31 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20=ED=94=BC=EA=B7=B8=EB=A7=88=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9(=EA=B0=84=EA=B2=A9=20=EB=B0=8F=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=ED=81=AC=EA=B8=B0=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/search/page.tsx | 45 ++++++++++++++++++------ src/components/search/Checklist.tsx | 2 +- src/components/search/Footer.tsx | 2 +- src/components/search/SearchTitle.tsx | 6 ++-- src/components/ui/TextArea.tsx | 32 +++++++++++------ src/components/ui/TextField.tsx | 49 ++++++++++++++++++++++----- 6 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/app/(main)/search/page.tsx b/src/app/(main)/search/page.tsx index c05a5f1..a29578c 100644 --- a/src/app/(main)/search/page.tsx +++ b/src/app/(main)/search/page.tsx @@ -61,7 +61,7 @@ export default function SearchPage() { return ( <div className="flex flex-col gap-13 px-20 pb-5"> - <div className="flex flex-col gap-5"> + <div className="flex flex-col gap-7"> <div className="flex justify-between"> <Title stepnum={1} title="발명 정보" /> <Button variant="secondary" size="sm" onClick={() => setIsModalOpen(true)}> @@ -69,13 +69,26 @@ export default function SearchPage() { </Button> </div> - <div className="flex flex-col gap-4"> - <TextField label="발명의 명칭" placeholder="EX) 생분해성 고분자 코팅 조성물" /> + <div className="flex flex-col gap-4 pl-10"> + <TextField + labelSize={17} + label="발명의 명칭" + placeholder="EX) 생분해성 고분자 코팅 조성물" + gap={1.5} + /> + <div className="flex flex-row gap-3"> - <TextField label="기술 분야" placeholder="EX) 고분자 화학 코팅" /> - <TextField label="IPC 분류" placeholder="EX) C09D 5/00" /> + <TextField + labelSize={17} + label="기술 분야" + placeholder="EX) 고분자 화학 코팅" + gap={1.5} + /> + <TextField labelSize={17} label="IPC 분류" placeholder="EX) C09D 5/00" gap={1.5} /> </div> + <TextArea + labelSize={17} label="핵심 기술 설명" placeholder="발명의 핵심 구성과 작동 방식을 간단히 설명해주세요" rows={4} @@ -83,11 +96,22 @@ export default function SearchPage() { </div> </div> - <div className="flex flex-col gap-5"> + <div className="flex flex-col gap-7"> <Title stepnum={2} title="출원인 정보" /> - <div className="flex flex-row gap-3"> - <TextField label="사명 (선택)" placeholder="EX) 그린폴리머(주)" /> - <TextField label="의뢰인 (선택)" placeholder="담당자 또는 발명자 성명을 입력해주세요" /> + + <div className="flex flex-row gap-3 pl-10"> + <TextField + labelSize={17} + label="사명 (선택)" + placeholder="EX) 그린폴리머(주)" + gap={1.5} + /> + <TextField + labelSize={17} + label="의뢰인 (선택)" + placeholder="담당자 또는 발명자 성명을 입력해주세요" + gap={1.5} + /> </div> </div> @@ -98,10 +122,11 @@ export default function SearchPage() { title="구성요소 분석" label="청구항을 구성요소 단위로 분해해 판단의 정밀성을 높힙니다." /> + <AICreationButton onClick={handleAICreate} /> </div> - <div className="overflow-hidden border-y border-outline-default"> + <div className="overflow-hidden border-y border-outline-default ml-10"> <ElementList elements={elements} isLoading={isLoading} diff --git a/src/components/search/Checklist.tsx b/src/components/search/Checklist.tsx index 3640a05..8a1ddca 100644 --- a/src/components/search/Checklist.tsx +++ b/src/components/search/Checklist.tsx @@ -9,7 +9,7 @@ export function Checklist() { const [checked, setChecked] = useState<Record<string, boolean>>({}); return ( - <div className="flex items-center gap-8"> + <div className="flex items-center gap-8 pl-10"> {ITEMS.map((label, index) => { const uniqueId = `checklist-${index}`; diff --git a/src/components/search/Footer.tsx b/src/components/search/Footer.tsx index a088402..7ecfb08 100644 --- a/src/components/search/Footer.tsx +++ b/src/components/search/Footer.tsx @@ -3,7 +3,7 @@ import { ResultCountButton } from "./ResultCountButton"; export function Footer() { return ( - <div className="flex items-center justify-between"> + <div className="flex pt-6 pb-7 pl-10 items-center justify-between"> <div className="flex items-center gap-6 text-label-17 text-title-secondary"> <span>탐색 결과 개수</span> <ResultCountButton /> diff --git a/src/components/search/SearchTitle.tsx b/src/components/search/SearchTitle.tsx index ae293f3..37138b1 100644 --- a/src/components/search/SearchTitle.tsx +++ b/src/components/search/SearchTitle.tsx @@ -6,14 +6,14 @@ type TitleProps = { export default function Title({ stepnum, title, label }: TitleProps) { return ( - <div className="flex flex-row gap-3 items-center"> + <div className="flex flex-row gap-4 items-center"> <div className="w-6 h-6 flex items-center justify-center rounded-sm bg-bg-primary-hover text-label-emphasis-15 text-inverse-on-primary"> {stepnum} </div> <div className="flex flex-row gap-2 items-center"> - <p className="text-title-primary text-body-emphasis-19">{title}</p> - <p className="text-caption-label text-label-13">{label}</p> + <p className="text-title-primary text-title-22">{title}</p> + <p className="text-caption-label text-label-15">{label}</p> </div> </div> ); diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx index ebe20ab..4037bb5 100644 --- a/src/components/ui/TextArea.tsx +++ b/src/components/ui/TextArea.tsx @@ -1,23 +1,33 @@ import { useId } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/cn"; -type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & { - label: string; - error?: string; - ref?: React.Ref<HTMLTextAreaElement>; -}; +const labelVariants = cva("flex items-center text-title-secondary", { + variants: { + labelSize: { + 13: "h-5 text-label-13", + 15: "h-5 text-label-15", + 17: "h-6 text-label-17", + }, + }, + defaultVariants: { labelSize: 13 }, +}); -export function TextArea({ label, error, className, id, ref, ...props }: TextAreaProps) { +type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & + VariantProps<typeof labelVariants> & { + label: string; + error?: string; + ref?: React.Ref<HTMLTextAreaElement>; + }; + +export function TextArea({ label, labelSize, error, className, id, ref, ...props }: TextAreaProps) { const autoId = useId(); const textareaId = id ?? autoId; const errorId = `${textareaId}-error`; return ( - <div className="flex w-full flex-col items-start gap-1"> - <label - htmlFor={textareaId} - className="flex h-5 items-center text-label-13 text-title-secondary" - > + <div className="flex w-full flex-col items-start gap-1.5"> + <label htmlFor={textareaId} className={labelVariants({ labelSize })}> {label} </label> <textarea diff --git a/src/components/ui/TextField.tsx b/src/components/ui/TextField.tsx index 3bed2e7..489ee65 100644 --- a/src/components/ui/TextField.tsx +++ b/src/components/ui/TextField.tsx @@ -1,20 +1,53 @@ import { useId } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/cn"; -type TextFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & { - label: string; - error?: string; - ref?: React.Ref<HTMLInputElement>; -}; +const labelVariants = cva("flex items-center text-title-secondary", { + variants: { + labelSize: { + 13: "h-5 text-label-13", + 15: "h-5 text-label-15", + 17: "h-6 text-label-17", + }, + }, + defaultVariants: { labelSize: 13 }, +}); -export function TextField({ label, error, className, id, ref, ...props }: TextFieldProps) { +const wrapperVariants = cva("flex w-full flex-col items-start", { + variants: { + gap: { + 1: "gap-1", + 1.5: "gap-1.5", + }, + }, + defaultVariants: { gap: 1 }, +}); + +type TextFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & + VariantProps<typeof labelVariants> & + VariantProps<typeof wrapperVariants> & { + label: string; + error?: string; + ref?: React.Ref<HTMLInputElement>; + }; + +export function TextField({ + label, + labelSize, + gap, + error, + className, + id, + ref, + ...props +}: TextFieldProps) { const autoId = useId(); const inputId = id ?? autoId; const errorId = `${inputId}-error`; return ( - <div className="flex w-full flex-col items-start gap-1"> - <label htmlFor={inputId} className="flex h-5 items-center text-label-13 text-title-secondary"> + <div className={wrapperVariants({ gap })}> + <label htmlFor={inputId} className={labelVariants({ labelSize })}> {label} </label> <input From cfb4d18951c3f0bd64ab2e6eeff8fa96a1fc49c9 Mon Sep 17 00:00:00 2001 From: girimNam <rlfla@naver.com> Date: Wed, 8 Jul 2026 16:00:26 +0900 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20sidebar=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=A7=80=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/sidebar/Sidebar.tsx | 37 +++++++++++++---------- src/components/sidebar/SidebarNavItem.tsx | 33 ++++++++++++-------- src/components/topbar/Topbar.tsx | 2 +- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 4e579b7..63c99e9 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -19,20 +19,26 @@ export function Sidebar({ open, onToggle }: SidebarProps) { return ( <aside - className={`flex h-full flex-col gap-5 border-r border-outline-sub px-3 py-5 transition-all duration-500 ${ - open ? "w-72" : "w-15" + className={`flex h-full flex-col gap-5 overflow-hidden border-r border-outline-sub py-5 transition-all duration-500 ${ + open ? "w-72 px-3" : "w-15 px-2" // 닫혔을 때 padding을 살짝 줄여주면 좁은 공간에서 밸런스가 더 좋습니다. }`} > - <div - className={`flex py-2 px-3 h-14 shrink-0 items-center ${open ? "justify-between" : "justify-center"}`} - > - {open && <IPXLogo width={57} height={20} />} - <button className="ml-auto cursor-pointer text-icon-neutral-default" onClick={onToggle}> + <div className={`flex h-14 shrink-0 items-center py-2 ${open ? "px-3" : "justify-center"}`}> + <div + className={`overflow-hidden transition-all ${open ? "w-14.25 opacity-100 duration-500 delay-200" : "w-0 opacity-0 duration-500"}`} + > + <IPXLogo width={57} height={20} className="shrink-0" /> + </div> + + <button + className={`cursor-pointer text-icon-neutral-default transition-transform duration-500 ${open ? "ml-auto" : ""}`} + onClick={onToggle} + > <IconViewSidebar width={20} height={20} className="fill-icon-neutral-default" /> </button> </div> - <nav className="flex flex-col gap-1 pb-5 border-b border-stroke-divider"> + <nav className="flex flex-col gap-1 border-b border-stroke-divider pb-5"> <SidebarNavItem href="/myhistory" icon={<MyHistory width={20} height={20} />} @@ -56,14 +62,13 @@ export function Sidebar({ open, onToggle }: SidebarProps) { /> </nav> - {open && ( - <div className="flex flex-1 flex-col gap-1.5 overflow-y-auto"> - <span className="px-3 text-label-13 text-title-primary">최근 탐색</span> - <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." /> - <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." /> - <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." /> - </div> //이후 api 연동시 map 이용해서 무한 스크롤 형식으로 변경 예정(지금은 위 3개만 봐주세용..) - )} + <div + className={`flex flex-1 flex-col gap-1.5 overflow-y-auto transition-opacity ${open ? "opacity-100 duration-300 delay-200" : "pointer-events-none opacity-0 duration-500"}`} + > + <span className="px-3 text-label-13 text-title-primary">최근 탐색</span> + <PreviousSearchItem href="#" label="니켈 회수율을 높일 수 있는 습식제련 기..." /> + {/* ... */} + </div> </aside> ); } diff --git a/src/components/sidebar/SidebarNavItem.tsx b/src/components/sidebar/SidebarNavItem.tsx index 4f242d2..985471d 100644 --- a/src/components/sidebar/SidebarNavItem.tsx +++ b/src/components/sidebar/SidebarNavItem.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/cn"; const sidebarNavItemVariants = cva( - "flex w-full cursor-pointer items-center gap-2 px-3 py-2 transition-colors", + "flex w-full cursor-pointer items-center py-2 transition-colors", // gap-2와 px-3를 제거하여 아래에서 조건부로 제어합니다. { variants: { active: { @@ -34,26 +34,33 @@ export function SidebarNavItem({ return ( <Link href={href} - className={cn(sidebarNavItemVariants({ active }), !open && "justify-center", className)} + className={cn( + sidebarNavItemVariants({ active }), + // 열렸을 땐 패딩과 간격을 주고, 닫혔을 땐 정중앙 배치 + open ? "px-3 gap-2 justify-start" : "px-0 justify-center", + className + )} > <div className={cn( - "flex shrink-0 items-center justify-center", + "flex shrink-0 items-center justify-center transition-transform duration-500", active ? "text-icon-primary-default" : "text-body-primary" + // 불필요한 translate-x 제거됨 )} > {icon} </div> - {open && ( - <span - className={cn( - "text-label-15", - active === true ? "text-primary-default" : "text-body-primary" - )} - > - {label} - </span> - )} + <span + className={cn( + "overflow-hidden whitespace-nowrap text-label-15 transition-[max-width,opacity]", + active === true ? "text-primary-default" : "text-body-primary", + open + ? "max-w-40 opacity-100 duration-300 delay-200" + : "max-w-0 pointer-events-none opacity-0 duration-500" + )} + > + {label} + </span> </Link> ); } diff --git a/src/components/topbar/Topbar.tsx b/src/components/topbar/Topbar.tsx index 08b6097..425f7cc 100644 --- a/src/components/topbar/Topbar.tsx +++ b/src/components/topbar/Topbar.tsx @@ -3,7 +3,7 @@ import { usePathname } from "next/navigation"; const PAGE_LABELS: Record<string, string> = { - "/history": "내 활동 기록", + "/myhistory": "내 활동 기록", "/search": "선행기술 탐색", "/analysis": "기술 분석", };