setAgreedPrivacy((checked) => !checked)}
+ onClose={() => setOpenTerms(null)}
+ />
+ )}
+
{submitError && (
{submitError}
diff --git a/src/app/(main)/myhistory/[id]/novelty/page.tsx b/src/app/(main)/myhistory/[id]/novelty/page.tsx
index 5d3c5db..97cc783 100644
--- a/src/app/(main)/myhistory/[id]/novelty/page.tsx
+++ b/src/app/(main)/myhistory/[id]/novelty/page.tsx
@@ -161,7 +161,7 @@ export default function NoveltyPage({ params }: { params: Promise<{ id: string }
diff --git a/src/app/(main)/search/loading/page.tsx b/src/app/(main)/search/loading/page.tsx
index 16a344c..b903c13 100644
--- a/src/app/(main)/search/loading/page.tsx
+++ b/src/app/(main)/search/loading/page.tsx
@@ -107,7 +107,7 @@ function LoadingContent() {
const handleStop = async () => {
if (!caseId) {
- router.push("/search");
+ router.push("/search?resume=1");
return;
}
@@ -115,7 +115,7 @@ function LoadingContent() {
setIsStopping(true);
try {
await cancelSearch(Number(caseId));
- router.push("/search");
+ router.push("/search?resume=1");
} catch (err) {
if (err instanceof ApiError) {
setCancelError(
@@ -132,7 +132,7 @@ function LoadingContent() {
};
useEffect(() => {
- if (!caseId) return;
+ if (!caseId || isStopping) return;
let cancelled = false;
let timer: ReturnType;
@@ -141,7 +141,7 @@ function LoadingContent() {
const poll = async () => {
try {
const result = await getSearchStatus(Number(caseId));
- if (cancelled) return;
+ if (cancelled || isStopping) return;
consecutiveErrors = 0;
setStatus(result);
setPollError(null);
@@ -179,7 +179,7 @@ function LoadingContent() {
cancelled = true;
clearTimeout(timer);
};
- }, [caseId, router, title]);
+ }, [caseId, isStopping, router, title]);
useEffect(() => {
if (caseId) return;
diff --git a/src/app/(main)/tech/[id]/page.tsx b/src/app/(main)/tech/[id]/page.tsx
index 912c6de..c01751e 100644
--- a/src/app/(main)/tech/[id]/page.tsx
+++ b/src/app/(main)/tech/[id]/page.tsx
@@ -14,6 +14,7 @@ import { getPriorArtDetail } from "@/lib/api/search";
import { ApiError } from "@/lib/api/error";
import { useKiprisThumbnail } from "@/hooks/useKiprisThumbnail";
import { formatPeriod } from "@/lib/priorArtFormat";
+import { buildOriginalDocumentUrl } from "@/lib/patentOriginalDocument";
import { RELEVANCE_LABEL, RELEVANCE_VARIANT, scoreToRelevance } from "@/lib/priorArtRelevance";
import type { PriorArtDetail } from "@/types/search.type";
@@ -139,6 +140,11 @@ export default function TechDetailPage() {
size="sm"
variant="secondary"
className="h-10.25 shrink-0 gap-1 rounded-md py-2.5 pr-4 pl-3"
+ disabled={!buildOriginalDocumentUrl(detail.applicationNumber)}
+ onClick={() => {
+ const url = buildOriginalDocumentUrl(detail.applicationNumber);
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
+ }}
>
원문보기
diff --git a/src/components/auth/TermsModal.tsx b/src/components/auth/TermsModal.tsx
new file mode 100644
index 0000000..b3614b6
--- /dev/null
+++ b/src/components/auth/TermsModal.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import CancelIcon from "@/components/icons/icon-cancel.svg";
+import { Radio } from "@/components/ui/Radio";
+import type { TermsContent } from "@/constants/auth/terms";
+
+type TermsModalProps = {
+ content: TermsContent;
+ agreementLabel: string;
+ checked: boolean;
+ onToggle: () => void;
+ onClose: () => void;
+};
+
+export function TermsModal({
+ content,
+ agreementLabel,
+ checked,
+ onToggle,
+ onClose,
+}: TermsModalProps) {
+ return (
+
+
e.stopPropagation()}
+ >
+
+
{content.title}
+
+
+
+
+ {content.sections.map((section) => (
+
+
{section.heading}
+ {section.paragraphs?.map((paragraph) => (
+
+ {paragraph}
+
+ ))}
+ {section.bullets && (
+
+ {section.bullets.map((bullet) => (
+ -
+ {bullet}
+
+ ))}
+
+ )}
+ {section.numbered && (
+
+ {section.numbered.map((item) => (
+ -
+ {item}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/myhistory/novelty/ComparisonResultDropdown.tsx b/src/components/myhistory/novelty/ComparisonResultDropdown.tsx
new file mode 100644
index 0000000..9570f3b
--- /dev/null
+++ b/src/components/myhistory/novelty/ComparisonResultDropdown.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import ExpandIcon from "@/components/icons/icon-expand.svg";
+import { cn } from "@/lib/cn";
+import type { NoveltyComparisonResult } from "@/types/novelty.type";
+
+const COMPARISON_RESULT_OPTIONS: Array<{
+ value: NoveltyComparisonResult;
+ label: string;
+}> = [
+ { value: "IDENTICAL", label: "동일" },
+ { value: "SIMILAR", label: "유사" },
+ { value: "NOVEL", label: "신규" },
+];
+
+interface ComparisonResultDropdownProps {
+ value: NoveltyComparisonResult;
+ disabled?: boolean;
+ onChange: (value: NoveltyComparisonResult) => void;
+}
+
+// SortingTag(검색결과 정렬 드롭다운)와 동일한 스타일 -> 값 동기화가 필요해서(취소 시 원래 값으로 복원 등) 완전히 controlled로 구현
+export function ComparisonResultDropdown({
+ value,
+ disabled,
+ onChange,
+}: ComparisonResultDropdownProps) {
+ const [open, setOpen] = useState(false);
+ const containerRef = useRef(null);
+ const selectedLabel = COMPARISON_RESULT_OPTIONS.find((option) => option.value === value)?.label;
+
+ useEffect(() => {
+ const handlePointerDown = (event: PointerEvent) => {
+ if (!containerRef.current?.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+
+ document.addEventListener("pointerdown", handlePointerDown);
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
+ }, []);
+
+ return (
+
+
+
+ {open && (
+
+ {COMPARISON_RESULT_OPTIONS.map((option) => {
+ const active = option.value === value;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/components/myhistory/novelty/Header.tsx b/src/components/myhistory/novelty/Header.tsx
index 63474fb..fa6d783 100644
--- a/src/components/myhistory/novelty/Header.tsx
+++ b/src/components/myhistory/novelty/Header.tsx
@@ -4,16 +4,18 @@ import { Button } from "@/components/ui/Button";
import ExternalIcon from "@/components/icons/icon-external.svg";
import { Chip } from "@/components/myhistory/ProjectCardChip";
import { useKiprisThumbnail } from "@/hooks/useKiprisThumbnail";
+import { buildOriginalDocumentUrl } from "@/lib/patentOriginalDocument";
interface HeaderProps {
title: string;
status: string;
- patentNumber: string;
+ applicationNumber: string;
organization: string;
}
-export default function Header({ title, status, patentNumber, organization }: HeaderProps) {
- const thumbnailUrl = useKiprisThumbnail(patentNumber);
+export default function Header({ title, status, applicationNumber, organization }: HeaderProps) {
+ const thumbnailUrl = useKiprisThumbnail(applicationNumber);
+ const originalDocumentUrl = buildOriginalDocumentUrl(applicationNumber);
return (
@@ -32,14 +34,22 @@ export default function Header({ title, status, patentNumber, organization }: He
-
{patentNumber}
+
{applicationNumber}
|
{organization}
-