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 ? (
+
+ ) : 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(", ")}` : ``}` : ``}
/>
}