-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/search #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/search #8
Changes from all commits
41a8de6
5a04142
7c34002
b76e7e9
0bbe416
cfb4d18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }; | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π Major | β‘ Quick win
π€ Prompt for AI Agentsπ©Ί Stability & Availability | π Major | β‘ Quick win λͺ¨λ¬ μ κ·Όμ±:
π€ Prompt for AI Agentsπ Maintainability & Code Quality | π Major | β‘ Quick win
β»οΈ μ μ: 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 |
||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π‘ Minor | β‘ Quick win textarea μλ λμ΄ μ‘°μ μ΄ μΈλΆ κ° λ³κ²½ μ λμνμ§ μμ΅λλ€.
π§ μ μνλ μμ : 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 |
||
|
|
||
| <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> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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 κΈ°λ° μ 리
π Committable suggestion
π€ Prompt for AI Agents