Skip to content
2 changes: 1 addition & 1 deletion src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })

<div className="flex flex-1 flex-col overflow-hidden">
<Topbar />
<main className="flex-1 overflow-auto p-10">{children}</main>
<main className="flex-1 overflow-auto p-10 scrollbar-hide">{children}</main>
</div>
</div>
);
Expand Down
152 changes: 149 additions & 3 deletions src/app/(main)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,153 @@
"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);
};
Comment on lines +33 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟑 Minor | ⚑ Quick win

handleAICreate의 setTimeout이 μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ μ •λ¦¬λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

μ»΄ν¬λ„ŒνŠΈκ°€ 2초 μ§€μ—° 쀑 μ–Έλ§ˆμš΄νŠΈλ˜λ©΄ setElements/setIsLoading이 μ–Έλ§ˆμš΄νŠΈλœ μ»΄ν¬λ„ŒνŠΈμ—μ„œ ν˜ΈμΆœλ©λ‹ˆλ‹€. useEffect 정리 ν•¨μˆ˜μ—μ„œ 타이머λ₯Ό μ·¨μ†Œν•˜κ±°λ‚˜, useRef둜 타이머 IDλ₯Ό μΆ”μ ν•˜μ—¬ μ •λ¦¬ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

♻️ μ œμ•ˆ: useEffect 기반 정리
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
  const handleAICreate = () => {
    setIsLoading(true);
-   setTimeout(() => {
+   timerRef.current = setTimeout(() => {
      setElements([...]);
      setIsLoading(false);
    }, 2000);
  };
+
+ useEffect(() => {
+   return () => {
+     if (timerRef.current) clearTimeout(timerRef.current);
+   };
+ }, []);
πŸ“ 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
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);
};
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleAICreate = () => {
setIsLoading(true);
timerRef.current = 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);
};
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
πŸ€– 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 `@src/app/`(main)/search/page.tsx around lines 33 - 60, `handleAICreate`
schedules a delayed state update with `setTimeout`, but there is no cleanup when
the search page component unmounts. Update the `page.tsx` component to track the
timer ID (for example with `useRef`) and clear it in a `useEffect` cleanup, or
otherwise ensure the timeout is canceled before unmount so `setElements` and
`setIsLoading` are never called on an unmounted component.


return (
<>
<div></div>
</>
<div className="flex flex-col gap-13 px-20 pb-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)}>
νŠΉν—ˆ 번호둜 뢈러였기
</Button>
</div>

<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
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}
/>
</div>
</div>

<div className="flex flex-col gap-7">
<Title stepnum={2} title="μΆœμ›μΈ 정보" />

<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>

<div className="flex flex-col gap-5">
<div className="flex flex-row items-center justify-between">
<Title
stepnum={3}
title="κ΅¬μ„±μš”μ†Œ 뢄석"
label="청ꡬ항을 κ΅¬μ„±μš”μ†Œ λ‹¨μœ„λ‘œ λΆ„ν•΄ν•΄ νŒλ‹¨μ˜ 정밀성을 λ†’νž™λ‹ˆλ‹€."
/>

<AICreationButton onClick={handleAICreate} />
</div>

<div className="overflow-hidden border-y border-outline-default ml-10">
<ElementList
elements={elements}
isLoading={isLoading}
onAdd={handleAdd}
onDelete={handleDelete}
onChange={handleChange}
/>
</div>

<Checklist />
</div>

<Footer />

{isModalOpen && (
<PatentImportModal
initialPatentNumber=""
onClose={() => setIsModalOpen(false)}
onSubmit={() => setIsModalOpen(false)}
/>
Comment on lines +144 to +149

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 | 🟠 Major | ⚑ Quick win

onSubmit 콜백이 νŠΉν—ˆλ²ˆν˜Έ 데이터λ₯Ό λ¬΄μ‹œν•©λ‹ˆλ‹€.

PatentImportModal은 onSubmit({ patentNumber })λ₯Ό ν˜ΈμΆœν•˜μ§€λ§Œ, νŽ˜μ΄μ§€μ˜ onSubmit ν•Έλ“€λŸ¬λŠ” 인자λ₯Ό λ°›μ§€ μ•Šκ³  λͺ¨λ‹¬λ§Œ λ‹«μŠ΅λ‹ˆλ‹€. μž…λ ₯된 νŠΉν—ˆλ²ˆν˜Έκ°€ μœ μ‹€λ©λ‹ˆλ‹€. API 연동 전이라면 TODOλ₯Ό 남기고, μ•„λ‹ˆλ©΄ patentNumberλ₯Ό ν™œμš©ν•˜λ„λ‘ μˆ˜μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.

πŸ€– 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, The Search page’s
PatentImportModal onSubmit handler is ignoring the submitted patentNumber, so
the value is lost. Update the inline onSubmit passed from search/page.tsx to
accept the modal payload from PatentImportModal and either use the patentNumber
for the next step or leave a TODO if API wiring is not ready; keep the existing
onClose behavior while ensuring the submit callback does not discard its
argument.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

λͺ¨λ‹¬ μ ‘κ·Όμ„±: role, aria-modal, Escape ν‚€, 포컀슀 트랩이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

PatentImportModal이 role="dialog" / aria-modal="true" 없이 <div>둜만 κ΅¬ν˜„λ˜μ–΄ 있고, Escape ν‚€λ‘œ λ‹«κΈ° 및 포컀슀 트랩이 μ—†μŠ΅λ‹ˆλ‹€. 슀크린 리더 μ‚¬μš©μžμ™€ ν‚€λ³΄λ“œ μ‚¬μš©μžκ°€ λͺ¨λ‹¬μ„ λ²—μ–΄λ‚  수 μ—†κ±°λ‚˜ λͺ¨λ‹¬μ΄ 열렀도 μΈμ§€ν•˜μ§€ λͺ»ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ€– 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, `PatentImportModal`의
μ ‘κ·Όμ„± 섀정이 λΆ€μ‘±ν•˜λ―€λ‘œ, λͺ¨λ‹¬ μ»¨ν…Œμ΄λ„ˆλ₯Ό `role="dialog"`와 `aria-modal="true"`λ₯Ό κ°–λŠ” μ§„μ§œ λ‹€μ΄μ–Όλ‘œκ·Έλ‘œ λ³΄κ°•ν•˜κ³ 
μ—΄λ¦° λ™μ•ˆ 배경으둜 ν¬μ»€μŠ€κ°€ μ΄λ™ν•˜μ§€ μ•Šλ„λ‘ 포컀슀 νŠΈλž©μ„ μΆ”κ°€ν•˜μ„Έμš”. λ˜ν•œ λͺ¨λ‹¬μ΄ 열릴 λ•Œ 첫 포컀슀λ₯Ό μ μ ˆν•œ μš”μ†Œμ— 두고, Escape ν‚€
μž…λ ₯으둜 `onClose`κ°€ ν˜ΈμΆœλ˜λ„λ‘ ν‚€λ³΄λ“œ μ²˜λ¦¬λ„ λ„£μ–΄μ•Ό ν•©λ‹ˆλ‹€. κ΅¬ν˜„ μœ„μΉ˜λŠ” `PatentImportModal` μ»΄ν¬λ„ŒνŠΈμ™€ 이λ₯Ό μ—¬λŠ”
`search/page.tsx`의 `isModalOpen` λ Œλ”λ§ 흐름을 ν•¨κ»˜ 확인해 μˆ˜μ •ν•˜μ„Έμš”.

πŸ“ Maintainability & Code Quality | 🟠 Major | ⚑ Quick win

PatentImportModal에 initialPatentNumber prop을 μ „λ‹¬ν•˜μ§€λ§Œ μ»΄ν¬λ„ŒνŠΈκ°€ 이λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

PatentImportModal의 μΈν„°νŽ˜μ΄μŠ€μ—λŠ” initialPatentNumberκ°€ μ„ μ–Έλ˜μ–΄ μžˆμœΌλ‚˜, μ‹€μ œ μ»΄ν¬λ„ŒνŠΈλŠ” 이λ₯Ό destructuringμ‘°μ°¨ ν•˜μ§€ μ•Šκ³  useState("")둜 μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€. λΆˆν•„μš”ν•œ prop을 μ œκ±°ν•˜κ±°λ‚˜, μ˜λ„λŒ€λ‘œ μ΄ˆκΈ°κ°’μœΌλ‘œ μ‚¬μš©ν•˜λ„λ‘ μˆ˜μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.

♻️ μ œμ•ˆ: initialPatentNumber μ‚¬μš© λ˜λŠ” 제거
- export function PatentImportModal({ onClose, onSubmit }: PatentImportModalProps) {
-   const [patentNumber, setPatentNumber] = useState("");
+ export function PatentImportModal({ initialPatentNumber = "", onClose, onSubmit }: PatentImportModalProps) {
+   const [patentNumber, setPatentNumber] = useState(initialPatentNumber);
πŸ€– 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 `@src/app/`(main)/search/page.tsx around lines 144 - 149, `PatentImportModal`
is receiving an `initialPatentNumber` prop from the search page, but the
component ignores it and always starts from an empty state. Update
`PatentImportModal` to either destructure and use `initialPatentNumber` when
initializing its local state, or remove the prop entirely from both the modal
usage in `Page` and the modal props/interface if it is not needed. Make sure the
fix is applied consistently in the `PatentImportModal` component and its caller
so the prop contract matches the actual behavior.

)}
</div>
);
}
3 changes: 3 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -68,6 +69,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 */
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
}
32 changes: 32 additions & 0 deletions src/components/search/Checklist.tsx
Original file line number Diff line number Diff line change
@@ -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 pl-10">
{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>
);
}
64 changes: 64 additions & 0 deletions src/components/search/ElementList/ElementList.tsx
Original file line number Diff line number Diff line change
@@ -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 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=" m-2 h-5 w-5 text-icon-primary-emphasize [&_path]:fill-current" />
κ΅¬μ„±μš”μ†Œ μΆ”κ°€
</button>
</>
)}
</div>
);
}
46 changes: 46 additions & 0 deletions src/components/search/ElementList/ElementRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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} />

<textarea
value={description}
onChange={(e) => {
onChangeDescription(e.target.value);
e.target.style.height = "auto";
e.target.style.height = `${e.target.scrollHeight}px`;
}}
placeholder="κ΅¬μ„±μš”μ†Œ μ„€λͺ…"
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"
/>
Comment on lines +25 to +35

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

textarea μžλ™ 높이 쑰정이 μ™ΈλΆ€ κ°’ λ³€κ²½ μ‹œ λ™μž‘ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

onChange ν•Έλ“€λŸ¬ λ‚΄μ—μ„œλ§Œ scrollHeight 기반 높이 쑰정이 μˆ˜ν–‰λ©λ‹ˆλ‹€. AI 생성(handleAICreate)으둜 인해 μƒˆ elementκ°€ κ΅μ²΄λ˜κ±°λ‚˜ 초기 λ Œλ” μ‹œ, textareaκ°€ μƒˆλ‘œ 마운트되면 인라인 heightκ°€ μ„€μ •λ˜μ§€ μ•Šμ•„ rows={1} λ†’μ΄λ‘œ κ³ μ •λ©λ‹ˆλ‹€. κΈ΄ μ„€λͺ… ν…μŠ€νŠΈκ°€ overflow-hidden으둜 잘렀 보일 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ”§ μ œμ•ˆν•˜λŠ” μˆ˜μ •: ref와 useEffect둜 마운트/κ°’ λ³€κ²½ μ‹œ 높이 μ‘°μ •
+import { useRef, useEffect } from "react";
 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) {
+  const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+  useEffect(() => {
+    const el = textareaRef.current;
+    if (el) {
+      el.style.height = "auto";
+      el.style.height = `${el.scrollHeight}px`;
+    }
+  }, [description]);
+
   return (
     <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-4 px-4 py-3">
       <ElementTitle index={index} value={name} onChange={onChangeName} />

       <textarea
+        ref={textareaRef}
         value={description}
         onChange={(e) => {
           onChangeDescription(e.target.value);
-          e.target.style.height = "auto";
-          e.target.style.height = `${e.target.scrollHeight}px`;
+          const el = textareaRef.current;
+          if (el) {
+            el.style.height = "auto";
+            el.style.height = `${el.scrollHeight}px`;
+          }
         }}
         placeholder="κ΅¬μ„±μš”μ†Œ μ„€λͺ…"
         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"
       />
πŸ€– 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 `@src/components/search/ElementList/ElementRow.tsx` around lines 25 - 35, The
textarea auto-resize logic in ElementRow currently only runs inside the onChange
handler, so it won’t update when description changes from outside the input,
such as after handleAICreate or on initial mount. Move the height adjustment
into ElementRow using a ref on the textarea plus a useEffect that runs whenever
description changes, and reuse the same scrollHeight-based sizing there so the
inline height is recalculated after remounts and external value updates.


<button
type="button"
onClick={onDelete}
className="p-2.5 text-icon-neutral-subtle transition-colors hover:text-error-default"
>
<TrashIcon className="h-6 w-6 [&_path]:fill-current" />
</button>
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/search/ElementList/ElementTitle.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
12 changes: 12 additions & 0 deletions src/components/search/ElementList/LoadingIndicator.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
18 changes: 18 additions & 0 deletions src/components/search/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Button } from "@/components/ui/Button";
import { ResultCountButton } from "./ResultCountButton";

export function Footer() {
return (
<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 />
</div>

<Button size="sm" disabled>
탐색 μ‹œμž‘
</Button>
{/* api 연동 이후 disabled ν•΄μ œ κ΄€λ ¨ 쑰건 μΆ”κ°€ μ˜ˆμ • */}
</div>
);
}
Loading
Loading