diff --git a/docs/README.md b/docs/README.md index 60e3139a..87e4dd76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ | 2 | ๐Ÿ“š **๋‹จ์–ด์žฅ ์ •๋ฆฌ ๋„๊ตฌ** | ๊ธ€์ž์ˆœ ์ •๋ ฌ, ํŠน์ • ๋‹จ์–ด ์ถ”์ถœ ๋“ฑ ๋‹จ์–ด์žฅ ๊ด€๋ฆฌ | | 3 | ๐Ÿ—„๏ธ **์˜คํ”ˆ DB ๋‹จ์–ด** | ๋‹จ์–ด ๋‹ค์šด๋กœ๋“œ ๋ฐ ๊ฒ€์ƒ‰ | | 4 | ๐Ÿ˜ˆ **๋นŒ๋Ÿฐ ๋‹จ์–ด์žฅ ๊ฒŒ์‹œ** | ๋นŒ๋Ÿฐ ๋‹จ์–ด์žฅ ๊ณต์œ  ๋ฐ ์—ด๋žŒ | -| 5 | โŒจ๏ธ **ํƒ€์ž ์—ฐ์Šต** | ๋„ํˆฌ ๋‹จ์–ด๋กœ ํƒ€์ž ์—ฐ์Šต | +| 5 | โŒจ๏ธ **ํƒ€์ž ์—ฐ์Šต** | *(์˜ˆ์ •)* ๋„ํˆฌ ๋‹จ์–ด๋กœ ํƒ€์ž ์—ฐ์Šต | | 6 | ๐Ÿ” **๋„์ฝ” ์ •๋ณด ์กฐํšŒ** | ํ”„๋กœํ•„, ๋žญํ‚น ๋“ฑ ์œ ์ € ์ •๋ณด ์กฐํšŒ | | 7 | โš”๏ธ **๋ฃจํŠธ์ „ ์—”์ง„** | *(์˜ˆ์ •)* ๋ฃจํŠธ์ „ ์ตœ์  ํ”Œ๋ ˆ์ด ๋ณด์กฐ ์—”์ง„ | diff --git a/scripts/release.mjs b/scripts/release.mjs index 3aee0d66..9ae2748b 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -104,9 +104,13 @@ async function run() { // Repo URL์ด ์žˆ์œผ๋ฉด ๋น„๊ต ๋งํฌ ์ƒ์„ฑ if (config.repoUrl) { - // ๋ฆด๋ฆฌ์ฆˆ ๋ชจ๋“œ์ผ ๋•Œ๋Š” ์ด์ „ ํƒœ๊ทธ์™€ ํ˜„์žฌ ๋ฒ„์ „ ๋น„๊ต ํ•„์š” (๊ตฌํ˜„ ์ƒ๋žต - ๋‹จ์ˆœํ™”) - // PR ๋ชจ๋“œ์ผ ๋•Œ๋Š” ํ˜„์žฌ ๋ฒ„์ „(old) .. ๋‹ค์Œ ๋ฒ„์ „(new) - const prevVersionTag = isReleaseMode ? '...' : `v${currentVersion}`; + // PR ๋ชจ๋“œ์ผ ๋•Œ๋Š” ํ˜„์žฌ ๋ฒ„์ „(old) .. ๋‹ค์Œ ๋ฒ„์ „(new) + // ๋ฆด๋ฆฌ์ฆˆ ๋ชจ๋“œ์ผ ๋•Œ๋Š” GitHub latest release ํƒœ๊ทธ(old) .. ํ˜„์žฌ ๋ฒ„์ „(new) + let prevVersionTag = `v${currentVersion}`; + if (isReleaseMode) { + prevVersionTag = await getPreviousReleaseTag(); + } + header = `# [${versionHeader}](${config.repoUrl}/compare/${prevVersionTag}...${versionHeader}) - ${date}`; } @@ -241,4 +245,37 @@ async function createGitHubRelease(version, body, dryRun) { console.log(`๐ŸŽ‰ GitHub Release published: ${release.html_url}`); } +async function getPreviousReleaseTag() { + const token = process.env.GITHUB_TOKEN; + + // GitHub API ์šฐ์„  ์‹œ๋„ (latest release) + if (token) { + try { + const octokit = getOctokit(token); + const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (latestRelease?.tag_name) { + return latestRelease.tag_name; + } + } catch (error) { + console.warn(`โš ๏ธ Failed to fetch latest release tag from GitHub API: ${error.message}`); + } + } + + // ํด๋ฐฑ: ๋กœ์ปฌ ์ตœ์‹  git ํƒœ๊ทธ + try { + const tags = await git.tags(); + if (tags.latest) { + return tags.latest; + } + } catch (error) { + console.warn(`โš ๏ธ Failed to fetch latest local git tag: ${error.message}`); + } + + throw new Error('Unable to resolve previous version tag from GitHub latest release or local git tags.'); +} + run(); diff --git a/src/__tests__/kkuko/shared/components/TryRenderImg.test.tsx b/src/__tests__/kkuko/shared/components/TryRenderImg.test.tsx index d8b24639..6e6ab4a2 100644 --- a/src/__tests__/kkuko/shared/components/TryRenderImg.test.tsx +++ b/src/__tests__/kkuko/shared/components/TryRenderImg.test.tsx @@ -38,7 +38,7 @@ describe('TryRenderImg', () => { render(); const img = screen.getByTestId('next-image'); expect(img).toBeInTheDocument(); - expect(img).toHaveAttribute('src', defaultProps.url); + expect(img).toHaveAttribute('src', expect.stringContaining(defaultProps.url)); expect(img).toHaveAttribute('alt', defaultProps.alt); }); @@ -61,13 +61,13 @@ describe('TryRenderImg', () => { // Expect src to have changed // url + ?r=1&ts=... - const expectedSrc1 = `${defaultProps.url}?r=1&ts=1234567890`; - expect(img).toHaveAttribute('src', expectedSrc1); + const expectedSrc1 = `r=1&ts=1234567890`; + expect(img).toHaveAttribute('src', expect.stringContaining(expectedSrc1)); // Second error -> retry 2 fireEvent.error(img); - const expectedSrc2 = `${defaultProps.url}?r=2&ts=1234567890`; - expect(img).toHaveAttribute('src', expectedSrc2); + const expectedSrc2 = `r=2&ts=1234567890`; + expect(img).toHaveAttribute('src', expect.stringContaining(expectedSrc2)); }); it('should show placeholder and call onFailure after maxRetries exceeded', () => { @@ -112,14 +112,14 @@ describe('TryRenderImg', () => { // Trigger one error to change state fireEvent.error(img); - expect(img).toHaveAttribute('src', expect.stringContaining('?r=1')); + expect(img).toHaveAttribute('src', expect.stringContaining('r=1')); // Change URL const newUrl = 'https://example.com/new.png'; rerender(); const newImg = screen.getByTestId('next-image'); - expect(newImg).toHaveAttribute('src', newUrl); + expect(newImg).toHaveAttribute('src', expect.stringContaining(newUrl)); // Should not have query params yet expect(newImg).not.toHaveAttribute('src', expect.stringContaining('?r=')); }); diff --git a/src/__tests__/manager-tool/extract/ContainX.test.tsx b/src/__tests__/manager-tool/extract/ContainX.test.tsx new file mode 100644 index 00000000..90aa022c --- /dev/null +++ b/src/__tests__/manager-tool/extract/ContainX.test.tsx @@ -0,0 +1,102 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import WordExtractorApp from "@/src/app/manager-tool/extract/containx/ContainX"; +import { getOutsideHelpModal } from "@/test/utils/dom"; + +jest.mock("@/app/manager-tool/extract/components/FileContentDisplay", () => { + return ({ onFileUpload, fileContent, resultData, resultTitle }: any) => ( +
+
File Content: {fileContent || "No content"}
+
Result Title: {resultTitle}
+
Result Count: {resultData?.length || 0}
+ +
{fileContent}
+
+ ); +}); + +describe("ContainX", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("์ดˆ๊ธฐ ๋ Œ๋”๋ง์ด ์ •์ƒ์ ์œผ๋กœ ๋˜๋Š”์ง€ ํ™•์ธ", () => { + render(); + + expect(screen.getByText("X๊ฐ€ ํฌํ•จ๋œ ๋‹จ์–ด ์ถ”์ถœ")).toBeInTheDocument(); + expect(screen.getAllByText("์„ค์ •")).toHaveLength(2); + expect(screen.getAllByText("์‹คํ–‰")).toHaveLength(2); + }); + + it("ํฌํ•จ๊ธ€์ž ์ž…๋ ฅ์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ", async () => { + const user = userEvent.setup(); + render(); + + const wordIncludeInput = getOutsideHelpModal(() => + screen.getAllByPlaceholderText("ํฌํ•จ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"), + ); + expect(wordIncludeInput).toBeDefined(); + + await user.type(wordIncludeInput!, "te"); + expect(wordIncludeInput).toHaveValue("te"); + }); + + it("ํŒŒ์ผ ๋‚ด์šฉ์ด ์—†์„ ๋•Œ ๋‹จ์–ด ์ถ”์ถœ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜๋Š”์ง€ ํ™•์ธ", () => { + render(); + + const extractButton = getOutsideHelpModal(() => + screen.getAllByText("๋‹จ์–ด ์ถ”์ถœ"), + ); + expect(extractButton).toBeDisabled(); + }); + + it("ํŒŒ์ผ ์—…๋กœ๋“œ ํ›„ ํฌํ•จ ๋‹จ์–ด ์ถ”์ถœ์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ", async () => { + const user = userEvent.setup(); + render(); + + const mockUploadButton = screen.getByText("Mock File Upload"); + await user.click(mockUploadButton); + + const wordIncludeInput = getOutsideHelpModal(() => + screen.getAllByPlaceholderText("ํฌํ•จ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"), + ); + await user.type(wordIncludeInput, "te"); + + const extractButton = getOutsideHelpModal(() => + screen.getAllByText("๋‹จ์–ด ์ถ”์ถœ"), + ); + await user.click(extractButton); + + await waitFor(() => { + const resultDisplay = screen.getByTestId("file-content-display"); + expect(resultDisplay).toHaveTextContent("Result Count: 2"); + }); + }); + + it("๋‹จ์–ด ์ถ”์ถœ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋˜๋Š”์ง€ ํ™•์ธ", async () => { + const user = userEvent.setup(); + render(); + + const downloadButton = getOutsideHelpModal(() => + screen.getAllByText("๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ"), + ); + expect(downloadButton).toBeDisabled(); + + const mockUploadButton = screen.getByText("Mock File Upload"); + await user.click(mockUploadButton); + + const wordIncludeInput = getOutsideHelpModal(() => + screen.getAllByPlaceholderText("ํฌํ•จ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"), + ); + await user.type(wordIncludeInput, "te"); + + const extractButton = getOutsideHelpModal(() => + screen.getAllByText("๋‹จ์–ด ์ถ”์ถœ"), + ); + await user.click(extractButton); + + await waitFor(() => { + expect(downloadButton).not.toBeDisabled(); + }); + }); +}); diff --git a/src/app/admin/request-docs/RequestDocsHome.tsx b/src/app/admin/request-docs/RequestDocsHome.tsx index 11331868..aec0bb70 100644 --- a/src/app/admin/request-docs/RequestDocsHome.tsx +++ b/src/app/admin/request-docs/RequestDocsHome.tsx @@ -8,6 +8,7 @@ import { Pagination, PaginationContent, PaginationItem, PaginationLink, Paginati import { SCM } from '@/src/app/lib/supabaseClient'; import { PostgrestError } from '@supabase/supabase-js'; import ErrorModal from '@/src/app/components/ErrModal'; +import Link from 'next/link'; type DocsWaitRequest = {id: number, req_at: string, docs_name: string, req_by: string | null, initial_consonant: boolean, req_byId: string | null} @@ -122,10 +123,12 @@ export default function DocsWaitManager({initialData}: {initialData?: DocsWaitRe
- + + +
diff --git a/src/app/admin/request-words/AdminWrapper.tsx b/src/app/admin/request-words/AdminWrapper.tsx index 912778d6..b7de133b 100644 --- a/src/app/admin/request-words/AdminWrapper.tsx +++ b/src/app/admin/request-words/AdminWrapper.tsx @@ -44,7 +44,7 @@ export default function AdminHomeWrapper(){ return; } - updateLoadingState(30, "๋‹จ์–ด ์‚ญ์ œ/์ถ”๊ฐ€ ์š”์ฒญ ๋‹จ์•„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘..."); + updateLoadingState(30, "๋‹จ์–ด ์‚ญ์ œ/์ถ”๊ฐ€ ์š”์ฒญ ๋‹จ์–ด ๊ฐ€์ ธ์˜ค๋Š” ์ค‘..."); const {data: waitWordsData, error: waitWordsError} = await SCM.get().allWaitWords(); if (waitWordsError){ MakeError(waitWordsError); diff --git a/src/app/manager-tool/extract/ExtractHome.tsx b/src/app/manager-tool/extract/ExtractHome.tsx index f9344290..7c201cdd 100644 --- a/src/app/manager-tool/extract/ExtractHome.tsx +++ b/src/app/manager-tool/extract/ExtractHome.tsx @@ -5,6 +5,7 @@ import { Type, Hash, ArrowRight, + Search, RotateCcw, Merge, FileText, @@ -37,15 +38,24 @@ const ExtractHome: React.FC = () => { }, { id: 3, + name: "X๊ฐ€ ํฌํ•จ๋œ ๋‹จ์–ด ์ถ”์ถœ", + description: "์ง€์ •ํ•œ ๊ธ€์ž๋‚˜ ๋ฌธ์ž์—ด์ด ํฌํ•จ๋œ ๋‹จ์–ด๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", + link: "/containx", + icon: Search, + color: "from-cyan-500 to-sky-500", + bgColor: "group-hover:bg-cyan-50 dark:group-hover:bg-cyan-950/20" + }, + { + id: 4, name: "X๋กœ ๋๋‚˜๋Š” ๋‹จ์–ด ์ถ”์ถœ", - description: "ํŠน์ • ์ ‘๋ฏธ์‚ฌ๋‚˜ ๋๊ธ€์ž๋กœ ๋งˆ๋ฌด๋ฆฌ๋˜๋Š” ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", + description: "ํŠน์ • ์ ‘๋ฏธ์‚ฌ๋กœ ๋งˆ๋ฌด๋ฆฌ๋˜๋Š” ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", link: "/endx", icon: Type, color: "from-purple-500 to-violet-500", bgColor: "group-hover:bg-purple-50 dark:group-hover:bg-purple-950/20" }, { - id: 4, + id: 5, name: "๋Œ๋ฆผ๋‹จ์–ด ์ถ”์ถœ", description: "ํšŒ๋ฌธ์ด๋‚˜ ๋Œ€์นญ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„ ํŠน๋ณ„ํ•œ ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", link: "/loop", @@ -54,7 +64,7 @@ const ExtractHome: React.FC = () => { bgColor: "group-hover:bg-orange-50 dark:group-hover:bg-orange-950/20" }, { - id: 5, + id: 6, name: "ํŒŒ์ผ ํ•ฉ์„ฑ", description: "์—ฌ๋Ÿฌ ํ…์ŠคํŠธ ํŒŒ์ผ์„ ํ•˜๋‚˜๋กœ ๋ณ‘ํ•ฉ์‹œํ‚ต๋‹ˆ๋‹ค.", link: "/merge", @@ -63,7 +73,7 @@ const ExtractHome: React.FC = () => { bgColor: "group-hover:bg-teal-50 dark:group-hover:bg-teal-950/20" }, { - id: 6, + id: 7, name: "ํ•œ๊ตญ์–ด ๋ฏธ์…˜๋‹จ์–ด ์ถ”์ถœ - A", description: "ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ์—์„œ ๋ฏธ์…˜ ์กฐ๊ฑด์— ๋งž๋Š” ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", link: "/korean-mission", @@ -72,7 +82,7 @@ const ExtractHome: React.FC = () => { bgColor: "group-hover:bg-pink-50 dark:group-hover:bg-pink-950/20" }, { - id: 7, + id: 8, name: "์˜์–ด ๋ฏธ์…˜๋‹จ์–ด ์ถ”์ถœ", description: "์˜์–ด ํ…์ŠคํŠธ์—์„œ ๋ฏธ์…˜ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", link: "/english-mission", @@ -81,7 +91,7 @@ const ExtractHome: React.FC = () => { bgColor: "group-hover:bg-indigo-50 dark:group-hover:bg-indigo-950/20" }, { - id: 8, + id: 9, name: "ํ•œ๊ตญ์–ด ๋ฏธ์…˜๋‹จ์–ด ์ถ”์ถœ - B", description: "1ํ‹ฐ์–ด ๋ฏธ์…˜๋‹จ์–ด๋งŒ์„ ์„ ๋ณ„ํ•˜์—ฌ ๋‹จ์–ด๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.", link: "/korean-mission-b", @@ -90,7 +100,7 @@ const ExtractHome: React.FC = () => { bgColor: "group-hover:bg-yellow-50 dark:group-hover:bg-yellow-950/20" }, { - id: 9, + id: 10, name: "๊ธฐ๋Šฅ ์ œ์•ˆํ•˜๊ธฐ", description: "์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜์‹œ๋‹ค๋ฉด ์–ธ์ œ๋“  ์š”์ฒญํ•ด์ฃผ์„ธ์š”!", icon: HelpCircle, diff --git a/src/app/manager-tool/extract/containx/ContainX.tsx b/src/app/manager-tool/extract/containx/ContainX.tsx new file mode 100644 index 00000000..c3a15351 --- /dev/null +++ b/src/app/manager-tool/extract/containx/ContainX.tsx @@ -0,0 +1,348 @@ +"use client"; +import React, { useState } from "react"; +import ErrorModal from "@/src/app/components/ErrModal"; +import type { ErrorMessage } from '@/src/app/types/type' +import Spinner from "@/src/app/components/Spinner"; +import FileContentDisplay from "../components/FileContentDisplay"; +import { Card, CardContent, CardHeader, CardTitle } from "@/src/app/components/ui/card"; +import { Button } from "@/src/app/components/ui/button"; +import { Input } from "@/src/app/components/ui/input"; +import { Label } from "@/src/app/components/ui/label"; +import { Checkbox } from "@/src/app/components/ui/checkbox"; +import { Badge } from "@/src/app/components/ui/badge"; +import { Download, Play, Settings, Zap, Home } from "lucide-react"; +import Link from "next/link"; +import HelpModal from "@/src/app/components/HelpModal"; + +const WordExtractorApp = () => { + const [file, setFile] = useState(null); + const [fileContent, setFileContent] = useState(null); + const [extractedWords, setExtractedWords] = useState([]); + const [sortChecked, setSortChecked] = useState(true); + const [errorModalView, seterrorModalView] = useState(null); + const [loading, setLoading] = useState(false); + const [wordInclude, setWordInclude] = useState(''); + + const handleFileUpload = (content: string) => { + setFileContent(content); + }; + + const handleError = (error: unknown) => { + if (error instanceof Error) { + seterrorModalView({ + ErrName: error.name, + ErrMessage: error.message, + ErrStackRace: error.stack, + inputValue: null + }); + } else { + seterrorModalView({ + ErrName: null, + ErrMessage: null, + ErrStackRace: error as string, + inputValue: null + }); + } + }; + + const extractWords = async () => { + try { + setLoading(true); + await new Promise(resolve => setTimeout(resolve, 1)) + + if (fileContent && wordInclude) { + const uniqueSet = new Set(); + const result: string[] = []; + const words = fileContent.split(/\s+/); + + words.forEach(word => { + const cleanedWord = word.replace(/[.,!?;:()]/g, ''); + if (cleanedWord && cleanedWord.includes(wordInclude) && !uniqueSet.has(cleanedWord)) { + uniqueSet.add(cleanedWord); + result.push(cleanedWord); + } + }); + + setExtractedWords(sortChecked ? result.sort((a, b) => a.localeCompare(b, "ko")) : result); + } + } catch (err) { + handleError(err); + } finally { + setLoading(false) + } + }; + + const downloadExtractedWords = () => { + try { + if (extractedWords.length === 0) return; + const blob = new Blob([extractedWords.join("\n")], { type: "text/plain" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `extracted_words_${wordInclude}_ํฌํ•จ๋ชฉ๋ก.txt`; + link.click(); + } catch (err) { + handleError(err); + } + }; + + return ( +
+
+
+
+
+
+ +
+
+

+ X๊ฐ€ ํฌํ•จ๋œ ๋‹จ์–ด ์ถ”์ถœ +

+

+ ํ…์ŠคํŠธ ํŒŒ์ผ์—์„œ ํŠน์ • ๋ฌธ์ž์—ด์ด ํฌํ•จ๋œ ๋‹จ์–ด๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค +

+
+
+
+ + + + +
+
+
+ 0 +

ํ…์ŠคํŠธ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•ฉ๋‹ˆ๋‹ค.

+
+
+ +
+
+ 1 +

์„ค์ •

+
+
+

ํฌํ•จํ•  ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: "๊ฐ€", "๋ฏธ์…˜")

+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ 2 +

์‹คํ–‰

+
+
+

์‹คํ–‰ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ณ  ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.

+
+ +
+
+
+ +
+
+ 3 +

๊ฒฐ๊ณผ ํ™•์ธ ๋ฐ ๋‹ค์šด๋กœ๋“œ

+
+
+

๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•œ ํ›„ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

+
+ +
+
+
+ +
+

์‚ฌ์šฉ ์˜ˆ์‹œ

+
+
+

์ž…๋ ฅ:

+
+                                                    ๋ฏธ์…˜
+                                                    ๊ฐ€์„
+                                                    ๋ด„๋‚ 
+                                                    ๊ฒจ์šธ
+                                                    ์ž„๋ฌด
+                                                
+
+
+
+
ํฌํ•จ๊ธ€์ž: "๋ฏธ" ์ถ”์ถœ
+
โ†“
+
+
+
+

์ถ”์ถœ ๊ฒฐ๊ณผ:

+
+
+
โ€ข ๋ฏธ์…˜
+
โ€ข ๋ฏธ๋กœ
+
+
+ ์ด 2๊ฐœ ๋‹จ์–ด ์ถ”์ถœ๋จ +
+
+
+
+
+ +
+

+ ๐Ÿ’ก ํŒ: ํฌํ•จ๊ธ€์ž์— ์—ฌ๋Ÿฌ ๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํ•ด๋‹น ๋ฌธ์ž์—ด์ด ๋“ค์–ด๊ฐ„ ๋‹จ์–ด๋งŒ ์ถ”์ถœ๋ฉ๋‹ˆ๋‹ค. +

+
+
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + ์„ค์ • + + + +
+ + setWordInclude(e.target.value)} + placeholder="ํฌํ•จ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + /> +
+ +
+ setSortChecked(checked as boolean)} + /> + +
+
+
+ + + + + + ์‹คํ–‰ + + + + + + + + + + {fileContent && ( + + +
+
+ {fileContent.split('\n').length} +
+
+ ํŒŒ์ผ์˜ ์ด ๋‹จ์–ด ์ˆ˜ +
+
+
+
+ )} +
+
+
+
+ + {errorModalView && ( + seterrorModalView(null)} + error={errorModalView} + /> + )} + + {loading && ( +
+
+ + ์ฒ˜๋ฆฌ ์ค‘์ž…๋‹ˆ๋‹ค... +
+
+ )} +
+ ); +}; + +export default WordExtractorApp; diff --git a/src/app/manager-tool/extract/containx/page.tsx b/src/app/manager-tool/extract/containx/page.tsx new file mode 100644 index 00000000..b39a2083 --- /dev/null +++ b/src/app/manager-tool/extract/containx/page.tsx @@ -0,0 +1,26 @@ +import WordExtractorApp from "./ContainX"; + +export async function generateMetadata() { + return { + title: "๋„์ฝ” ์œ ํ‹ธ๋ฆฌํ‹ฐ - ๋‹จ์–ด์žฅ ๊ด€๋ฆฌ", + description: '๋„์ฝ” ์œ ํ‹ธ๋ฆฌํ‹ฐ - ๋‹จ์–ด์žฅ ๊ด€๋ฆฌ', + openGraph: { + title: "๋„์ฝ” ์œ ํ‹ธ๋ฆฌํ‹ฐ - ๋‹จ์–ด์žฅ ๊ด€๋ฆฌ", + description: "๋„์ฝ” ์œ ํ‹ธ๋ฆฌํ‹ฐ - ๋‹จ์–ด์žฅ ๊ด€๋ฆฌ", + type: "website", + url: "https://kkuko-utils.vercel.app/manager-tool/extract/containx", + siteName: "๋„์ฝ” ์œ ํ‹ธ๋ฆฌํ‹ฐ", + locale: "ko_KR", + }, + }; +} + +const ContainXPage: React.FC = () => { + return ( + <> + + + ) +} + +export default ContainXPage; diff --git a/src/app/release-note/ReleaseNote.tsx b/src/app/release-note/ReleaseNote.tsx index ffa4bfa3..c0c47c79 100644 --- a/src/app/release-note/ReleaseNote.tsx +++ b/src/app/release-note/ReleaseNote.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import { SCM } from '../lib/supabaseClient'; import ErrorModal from '../components/ErrModal'; import { Calendar, Clock, ChevronRight } from 'lucide-react'; +import MarkdownViewer from '../components/MarkdownViewer'; interface Note { id: number; @@ -12,36 +13,78 @@ interface Note { link: string | null; } +interface GithubRelease { + id: number; + name: string; + body: string; + published_at: string; + html_url: string; + tag_name: string; +} + +type ReleaseTab = 'internal' | 'github'; + const ReleaseNote = () => { const [notes, setNotes] = useState([]); + const [githubNotes, setGithubNotes] = useState([]); const [loading, setLoading] = useState(true); + const [githubError, setGithubError] = useState(null); const [errorModalView, setErrorModalView] = useState(null); - const [expandedNote, setExpandedNote] = useState(null); + const [activeTab, setActiveTab] = useState('internal'); + const [expandedNote, setExpandedNote] = useState(null); useEffect(() => { - const fetchNotes = async () => { + const fetchData = async () => { setLoading(true); - const { data, error } = await SCM.get().releaseNote(); - - if (error) { + + const [internalResult, githubResult] = await Promise.allSettled([ + SCM.get().releaseNote(), + fetch('https://api.github.com/repos/SolidLoop-studio/kkuko-utils/releases', { + headers: { + Accept: 'application/vnd.github+json' + } + }) + ]); + + if (internalResult.status === 'fulfilled') { + const { data, error } = internalResult.value; + if (error) { + setErrorModalView({ + ErrName: error.name, + ErrMessage: error.message, + ErrStackRace: error.code, + inputValue: "๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ" + }); + } else { + setNotes(data); + } + } else { setErrorModalView({ - ErrName: error.name, - ErrMessage: error.message, - ErrStackRace: error.code, + ErrName: 'Supabase Error', + ErrMessage: internalResult.reason instanceof Error ? internalResult.reason.message : '๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.', + ErrStackRace: '', inputValue: "๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ" }); - setLoading(false); - return; } - - setNotes(data); + + if (githubResult.status === 'fulfilled') { + if (!githubResult.value.ok) { + setGithubError(`GitHub ๋ฆด๋ฆฌ์ฆˆ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. (HTTP ${githubResult.value.status})`); + } else { + const data = (await githubResult.value.json()) as GithubRelease[]; + setGithubNotes(data); + } + } else { + setGithubError('GitHub ๋ฆด๋ฆฌ์ฆˆ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + setLoading(false); }; - - fetchNotes(); + + fetchData(); }, []); - const toggleExpand = (id: number) => { + const toggleExpand = (id: string) => { if (expandedNote === id) { setExpandedNote(null); } else { @@ -88,7 +131,38 @@ const ReleaseNote = () => {
- {notes.length === 0 ? ( +
+
+ + +
+
+ + {activeTab === 'internal' && (notes.length === 0 ? (

๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

@@ -96,7 +170,8 @@ const ReleaseNote = () => {
{notes.map((note) => { const { date, time } = formatDate(note.created_at); - const isExpanded = expandedNote === note.id; + const noteKey = `internal-${note.id}`; + const isExpanded = expandedNote === noteKey; return (
{ >
toggleExpand(note.id)} + onClick={() => toggleExpand(noteKey)} >

@@ -153,6 +228,80 @@ const ReleaseNote = () => { ); })}

+ ))} + + {activeTab === 'github' && ( + githubError ? ( +
+

{githubError}

+
+ ) : githubNotes.length === 0 ? ( +
+

GitHub ๋ฆด๋ฆฌ์ฆˆ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

+
+ ) : ( +
+ {githubNotes.map((note) => { + const { date, time } = formatDate(note.published_at); + const noteKey = `github-${note.id}`; + const isExpanded = expandedNote === noteKey; + + return ( +
+
toggleExpand(noteKey)} + > +
+

+ {note.name || note.tag_name} +

+
+
+ + {date} +
+
+ + {time} +
+ + {note.tag_name} + +
+
+ +
+ + {isExpanded && ( + + )} +
+ ); + })} +
+ ) )} {errorModalView && setErrorModalView(null)} />}
diff --git a/src/app/word-combiner/WordCombinerClient.tsx b/src/app/word-combiner/WordCombinerClient.tsx index d58914d7..b70d6b63 100644 --- a/src/app/word-combiner/WordCombinerClient.tsx +++ b/src/app/word-combiner/WordCombinerClient.tsx @@ -90,9 +90,9 @@ export default function WordCombinerClient({ prop }: { prop: WordCombinerWithDat setLoading(true); setTimeout(() => { const manger6 = new CombinationManager(normalJOKAK.replace(/\s+/g, '').split('').sort().join(''), len6WordsData); - setLen6Date(manger6.getBests()); + setLen6Date(manger6.getBests().sort()); const manger5 = new CombinationManager(manger6.remainStr(), len5WordsData); - setLen5Data(manger5.getBests()); + setLen5Data(manger5.getBests().sort()); setLoading(false); setNormalJOKAK(manger5.remainStr()); }, 1) @@ -122,9 +122,9 @@ export default function WordCombinerClient({ prop }: { prop: WordCombinerWithDat setLoading(true); setTimeout(() => { const manger6 = new CombinationManager(highJOKAK.replace(/\s+/g, '').split('').sort().join(''), len6WordsData); - setLen6Date(manger6.getBests()); + setLen6Date(manger6.getBests().sort()); const manger5 = new CombinationManager(manger6.remainStr(), len5WordsData); - setLen5Data(manger5.getBests()); + setLen5Data(manger5.getBests().sort()); setLoading(false); setHighJOKAK(manger5.remainStr()); }, 1) @@ -154,9 +154,9 @@ export default function WordCombinerClient({ prop }: { prop: WordCombinerWithDat setLoading(true); setTimeout(() => { const manger6 = new CombinationManager(rareJOKAK.replace(/\s+/g, '').split('').sort().join(''), len6WordsData); - setLen6Date(manger6.getBests()); + setLen6Date(manger6.getBests().sort()); const manger5 = new CombinationManager(manger6.remainStr(), len5WordsData); - setLen5Data(manger5.getBests()); + setLen5Data(manger5.getBests().sort()); setLoading(false); setRareJOKAK(manger5.remainStr()); }, 1) diff --git a/src/app/word/search/[query]/WordInfo.tsx b/src/app/word/search/[query]/WordInfo.tsx index ddf604fb..2a06c4c6 100644 --- a/src/app/word/search/[query]/WordInfo.tsx +++ b/src/app/word/search/[query]/WordInfo.tsx @@ -565,7 +565,12 @@ const WordInfo = ({ wordInfo }: { wordInfo: WordInfoProps }) => { {completeModalOpen && 0 ? `์ถ”๊ฐ€ ์š”์ฒญ๋œ ์ฃผ์ œ: ${completeModalOpen.addThemes.join(", ")}` : ``} ${completeModalOpen.delThemes.length > 0 ? `์‚ญ์ œ ์š”์ฒญ๋œ ์ฃผ์ œ: ${completeModalOpen.delThemes.join(", ")}` : ``}` : ``} /> }