- {features.map((feature, index) => (
-
-
-
{feature.title}
-
{feature.description}
-
-
- 바로가기 →
-
+
+ {/* 헤더 */}
+
+
+
+
+
+ 추가 기능
+
- ))}
+
+ 메인기능 외에도 다양한 유틸리티 기능을 제공합니다.
+
+
+
+ {/* 기능 카드 그리드 */}
+
+ {features.map((feature, index) => {
+ const IconComponent = feature.icon;
+
+ return feature.link ? (
+
+
+
+
+
+ {feature.title}
+
+
+
+ {feature.description}
+
+
+
+
+ 시작하기 →
+
+
+
+
+ ) : (
+
+
+
+
+ {feature.title}
+
+
+
+ {feature.description}
+
+
+
+
+ 제작 예정
+
+
+
+ );
+ })}
+
);
diff --git a/app/header.tsx b/app/header.tsx
index 3373708..91284b9 100644
--- a/app/header.tsx
+++ b/app/header.tsx
@@ -3,11 +3,12 @@ import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useDispatch, useSelector } from 'react-redux';
-import { Menu, X, User, ChevronDown } from 'lucide-react';
+import { Menu, X, User, ChevronDown, LayoutDashboard, Sun, Moon } from 'lucide-react';
import type { RootState, AppDispatch } from "./store/store";
-import { supabase } from "./lib/supabaseClient";
+import { SCM } from "./lib/supabaseClient";
import { useRouter } from "next/navigation";
import { userAction } from "./store/slice";
+import { useTheme } from 'next-themes'
const Header = () => {
const [isOpen, setIsOpen] = useState
(false);
@@ -15,13 +16,15 @@ const Header = () => {
const mobileMenuRef = useRef(null);
const profileDropdownRef = useRef(null);
const isLoggedIn = useSelector((state: RootState) => state.user.username) !== undefined;
- const username = useSelector((state: RootState) => state.user.username);
+ const user = useSelector((state: RootState) => state.user);
const pathname = usePathname();
const router = useRouter();
const dispatch = useDispatch();
+ const username=user.username;
+ const { theme, setTheme } = useTheme();
const handleLogout = async () => {
- await supabase.auth.signOut();
+ await SCM.logout();
dispatch(
userAction.setInfo({
username: undefined,
@@ -57,6 +60,10 @@ const Header = () => {
setIsProfileOpen(false);
}, [pathname]);
+ const toggleTheme = () => {
+ setTheme(theme === 'dark' ? 'light' : 'dark');
+ }
+
const navItems = [
{ href: "/word-combiner", label: "단어조합기", isActive: pathname === "/word-combiner" },
{ href: "/manager-tool", label: "단어장 관리 도구", isActive: pathname.includes('manager-tool') },
@@ -111,56 +118,81 @@ const Header = () => {
))}
- {/* 데스크톱 프로필 드롭다운 */}
-
+ {/* 데스크톱 테마 토글 & 프로필 영역 */}
+
+ {/* 테마 토글 버튼 */}
- {isProfileOpen && (
-
-
- {isLoggedIn ? (
- <>
-
안녕하세요,
-
{username}님
- >
- ) : (
-
로그인하세요
- )}
+ {/* 프로필 드롭다운 */}
+
+
@@ -168,7 +200,7 @@ const Header = () => {
@@ -188,6 +220,27 @@ const Header = () => {
{/* 모바일 프로필 섹션 */}
+ {/* 모바일 테마 토글 버튼 */}
+
+
+ {theme === 'dark' ? (
+ <>
+
+ 라이트 모드
+ >
+ ) : (
+ <>
+
+ 다크 모드
+ >
+ )}
+
+
+
{isLoggedIn ? (
<>
@@ -205,6 +258,14 @@ const Header = () => {
>
로그아웃
+ {['r4','admin'].includes(user.role) && (
+
+ 관리자 페이지
+
+ )}
>
) : (
) {
return (
-
+
+ className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen dark:bg-gray-900`}>
{ process.env.NODE_ENV === "production" && (<>
+
+ {/* ㄱㄴㄷ 순 정렬 v3 */}
+
+
ㄱㄴㄷ 순 정렬 v3:
+
+ 정렬하기
+
+
+
+
+
+
+ {/* 길이 긴순 정렬 */}
+
+ 길이 긴 순 정렬:
+
+ 정렬하기
+
+
diff --git a/app/manager-tool/arrange/HelpModal.tsx b/app/manager-tool/arrange/HelpModal.tsx
index dd7e4ae..1c1df45 100644
--- a/app/manager-tool/arrange/HelpModal.tsx
+++ b/app/manager-tool/arrange/HelpModal.tsx
@@ -2,13 +2,14 @@ import React, { useEffect, useRef } from 'react';
import HelpModal from '@/app/components/HelpModal';
interface HelpModalprop {
- wantGo?: 1 | 2 | 3;
+ wantGo?: 1 | 2 | 3 | 4;
}
const HelpModalB = ({ wantGo }: HelpModalprop) => {
// 각 섹션에 대한
const sortRef1 = useRef
(null);
const sortRef2 = useRef(null);
+ const sortRef3 = useRef(null);
const scrollToSection = (ref: React.RefObject) => {
if (ref.current) {
@@ -25,6 +26,9 @@ const HelpModalB = ({ wantGo }: HelpModalprop) => {
case 3:
scrollToSection(sortRef2);
break;
+ case 4:
+ scrollToSection(sortRef3);
+ break;
}
}
});
@@ -49,6 +53,14 @@ const HelpModalB = ({ wantGo }: HelpModalprop) => {
ㄱㄴㄷ순 정렬 v2
+
+ scrollToSection(sortRef3)}
+ >
+ ㄱㄴㄷ순 정렬 v3
+
+
{/* 본문 */}
@@ -82,6 +94,21 @@ const HelpModalB = ({ wantGo }: HelpModalprop) => {
{`나릅\n개두릅\n주릅\n사릅`}
→
{`=[개]=\n개두릅\n\n=[나]=\n나릅\n\n=[사]=\n사릅\n\n=[주]=\n주릅`}
+
+
+ ㄱㄴㄷ순 정렬 v3
+
+
+
+ v3 버전은 한글 앞글자순으로 정렬하고 길이별로 정렬합니다.
+
+
+
+ {`가끔가끔\n가격\n오늘\n오글오글`}
→
{`가끔가끔\n가격\n오글오글\n오늘`}
+
)
diff --git a/app/manager-tool/extract/components/FileContentDisplay.tsx b/app/manager-tool/extract/components/FileContentDisplay.tsx
index c12b93b..dc944e0 100644
--- a/app/manager-tool/extract/components/FileContentDisplay.tsx
+++ b/app/manager-tool/extract/components/FileContentDisplay.tsx
@@ -73,7 +73,7 @@ const VirtualizedTextViewer = React.memo(({
if (!content) {
return (
-
+
{placeholder}
);
@@ -83,15 +83,15 @@ const VirtualizedTextViewer = React.memo(({
{searchable && (
-
+
setSearchTerm(e.target.value)}
- className="pl-10 text-sm"
+ className="pl-10 text-sm bg-background border-input dark:border-input text-foreground dark:text-foreground placeholder:text-muted-foreground dark:placeholder:text-muted-foreground dark:bg-gray-800 dark:border-gray-700"
/>
{searchTerm && (
-
+
{filteredLines.length}줄
)}
@@ -99,7 +99,7 @@ const VirtualizedTextViewer = React.memo(({
)}
@@ -119,14 +119,14 @@ const VirtualizedTextViewer = React.memo(({
width: '100%'
}}
>
-
+
{visibleLines.join('\n')}
) : (
// 일반 크기 텍스트 - 전체 렌더링
-
+
{filteredLines.join('\n')}
)}
@@ -134,7 +134,7 @@ const VirtualizedTextViewer = React.memo(({
{isLargeContent && (
-
+
대용량 파일 - 가상화 모드 ({filteredLines.length.toLocaleString()}줄)
)}
@@ -206,16 +206,16 @@ const FileContentDisplay = ({
return (
{/* File Upload Section */}
-
+
-
+
파일 업로드
-
+
{file && (
-
+
-
- {file.name}
-
+
+ {file.name}
+
{(file.size / 1024).toFixed(1)} KB
-
+
초기화
@@ -247,13 +247,13 @@ const FileContentDisplay = ({
{/* Content Display Section */}
{/* File Content */}
-
+
-
+
업로드된 파일 내용
{fileContent && (
-
+
{fileContent.split('\n').length.toLocaleString()}줄
)}
@@ -262,7 +262,7 @@ const FileContentDisplay = ({
{loading ? (
) : (
{/* Results Display */}
-
+
-
+
{resultTitle}
{resultData.length > 0 && (
-
+
{resultData.length}개
)}
diff --git a/app/manager-tool/extract/endx/EndX.tsx b/app/manager-tool/extract/endx/EndX.tsx
index 16d82b3..6b95685 100644
--- a/app/manager-tool/extract/endx/EndX.tsx
+++ b/app/manager-tool/extract/endx/EndX.tsx
@@ -52,11 +52,21 @@ const WordExtractorApp = () => {
try {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 1))
- console.log(fileContent?.length, wordEnd)
+
if (fileContent && wordEnd) {
const words = fileContent.split(/\s+/).filter((word) => word.endsWith(wordEnd));
- setExtractedWords(sortChecked ? words.sort((a, b) => a.localeCompare(b, "ko")) : words);
- console.log(words.length)
+ // 중복 제거
+ const uniqueSet = new Set();
+ const result: string[] = [];
+ words.forEach(word => {
+ const cleanedWord = word.replace(/[.,!?;:()]/g, ''); // 특수문자 제거
+ if (cleanedWord && !uniqueSet.has(cleanedWord)) {
+ uniqueSet.add(cleanedWord);
+ result.push(cleanedWord);
+ }
+ });
+ setExtractedWords(sortChecked ? result.sort((a, b) => a.localeCompare(b, "ko")) : result);
+
}
} catch (err) {
handleError(err);
@@ -100,7 +110,7 @@ const WordExtractorApp = () => {
-
+
도구홈
@@ -108,7 +118,7 @@ const WordExtractorApp = () => {
{/* Step 0 */}
@@ -127,13 +137,13 @@ const WordExtractorApp = () => {
원하는 끝글자를 입력합니다. (예: "다", "션", "시리즈")
-
+
-
+
-
+
@@ -148,7 +158,7 @@ const WordExtractorApp = () => {
실행 버튼을 누르고 기다립니다.
-
+
단어 추출
@@ -165,7 +175,7 @@ const WordExtractorApp = () => {
결과를 확인한 후 다운로드합니다.
-
+
결과 다운로드
@@ -180,8 +190,8 @@ const WordExtractorApp = () => {
사용 예시
-
입력:
-
+ 입력:
+
이름
하품
늠름
@@ -191,27 +201,26 @@ const WordExtractorApp = () => {
-
끝글자: "름" 추출
-
↓
+
끝글자: "름" 추출
+
↓
-
추출 결과:
-
-
-
-
+
+
💡 팁: 정렬 옵션을 체크하면 결과가 가나다순으로 정렬됩니다.
@@ -242,17 +251,16 @@ const WordExtractorApp = () => {
{/* Control Panel - 1/4 width */}
- {/* Settings Card */}
-
+
-
-
+
+
설정
-
+
{
/>
-
- {/* Actions Card */}
-
+
-
-
+
+
실행
@@ -314,7 +320,7 @@ const WordExtractorApp = () => {
{/* Status Card */}
{fileContent && (
-
+
diff --git a/app/manager-tool/extract/english-mission/EnglishMission.tsx b/app/manager-tool/extract/english-mission/EnglishMission.tsx
index 3c65643..a14b841 100644
--- a/app/manager-tool/extract/english-mission/EnglishMission.tsx
+++ b/app/manager-tool/extract/english-mission/EnglishMission.tsx
@@ -126,7 +126,7 @@ const WordExtractorApp = () => {
-
+
도구홈
@@ -134,32 +134,32 @@ const WordExtractorApp = () => {
{/* Step 0 */}
- 0
-
텍스트 파일을 업로드 합니다.
+ 0
+ 텍스트 파일을 업로드 합니다.
{/* Step 1 */}
- 1
-
설정
+ 1
+ 설정
-
미션글자가 최수 몇개 이상 포함되어 있어야 하는지 입력합니다.
-
+
미션글자가 최수 몇개 이상 포함되어 있어야 하는지 입력합니다.
+
-
+
-
+
@@ -169,12 +169,12 @@ const WordExtractorApp = () => {
{/* Step 2 */}
- 2
-
실행
+ 2
+ 실행
-
실행 버튼을 누르고 기다립니다.
-
+
실행 버튼을 누르고 기다립니다.
+
단어 추출
@@ -186,12 +186,12 @@ const WordExtractorApp = () => {
{/* Step 3 */}
- 3
-
결과 확인 및 다운로드
+ 3
+ 결과 확인 및 다운로드
-
결과를 확인한 후 다운로드합니다.
-
+
결과를 확인한 후 다운로드합니다.
+
결과 다운로드
@@ -203,11 +203,11 @@ const WordExtractorApp = () => {
{/* 예시 */}
-
사용 예시
+
사용 예시
-
입력:
-
+ 입력:
+
error
computer
nano
@@ -218,14 +218,14 @@ const WordExtractorApp = () => {
-
최소포함수: 1 추출
-
↓
+
최소포함수: 1 추출
+
↓
-
추출 결과:
-
-
+
추출 결과:
+
+
• error [r:3 e:1 o:1]
• computer [c:1 e:1 m:1 o:1 p:1 r:1 t:1 u:1]
• nano [n:2 a:1 o:1]
@@ -238,8 +238,8 @@ const WordExtractorApp = () => {
-
-
+
+
💡 팁: 정렬 옵션을 체크하면 결과가 가나다순으로 정렬됩니다.
@@ -271,16 +271,16 @@ const WordExtractorApp = () => {
{/* Settings Card */}
-
+
-
-
+
+
설정
-
+
{
/>
@@ -308,10 +308,10 @@ const WordExtractorApp = () => {
{/* Actions Card */}
-
+
-
-
+
+
실행
@@ -344,7 +344,7 @@ const WordExtractorApp = () => {
{/* Status Card */}
{fileContent && (
-
+
diff --git a/app/manager-tool/extract/korean-mission-b/KoreanMissionB.tsx b/app/manager-tool/extract/korean-mission-b/KoreanMissionB.tsx
index ea1b29c..11d9175 100644
--- a/app/manager-tool/extract/korean-mission-b/KoreanMissionB.tsx
+++ b/app/manager-tool/extract/korean-mission-b/KoreanMissionB.tsx
@@ -257,10 +257,10 @@ const WordExtractorApp = () => {
{/* Settings Card */}
-
+
-
-
+
+
설정
@@ -273,7 +273,7 @@ const WordExtractorApp = () => {
/>
@@ -282,10 +282,10 @@ const WordExtractorApp = () => {
{/* Actions Card */}
-
+
-
-
+
+
실행
@@ -318,7 +318,7 @@ const WordExtractorApp = () => {
{/* Status Card */}
{fileContent && (
-
+
diff --git a/app/manager-tool/extract/korean-mission/KoreanMission.tsx b/app/manager-tool/extract/korean-mission/KoreanMission.tsx
index 29e7181..98d6385 100644
--- a/app/manager-tool/extract/korean-mission/KoreanMission.tsx
+++ b/app/manager-tool/extract/korean-mission/KoreanMission.tsx
@@ -436,10 +436,10 @@ const WordExtractorApp = () => {
{/* Settings Card */}
-
+
-
-
+
+
설정
@@ -452,7 +452,7 @@ const WordExtractorApp = () => {
/>
@@ -465,7 +465,7 @@ const WordExtractorApp = () => {
/>
@@ -473,7 +473,7 @@ const WordExtractorApp = () => {
-
+
{selected.length}/3
@@ -490,7 +490,7 @@ const WordExtractorApp = () => {
/>
@@ -508,10 +508,10 @@ const WordExtractorApp = () => {
{/* Actions Card */}
-
+
-
-
+
+
실행
@@ -544,7 +544,7 @@ const WordExtractorApp = () => {
{/* Status Card */}
{fileContent && (
-
+
diff --git a/app/manager-tool/extract/lenx/LenX.tsx b/app/manager-tool/extract/lenx/LenX.tsx
index c610d45..6bf9d41 100644
--- a/app/manager-tool/extract/lenx/LenX.tsx
+++ b/app/manager-tool/extract/lenx/LenX.tsx
@@ -55,7 +55,16 @@ const WordExtractorApp = () => {
if (fileContent) {
// 길이에 맞는 단어 추출
const words = fileContent.split(/\s+/).filter((word) => word.length === wordLength);
- setExtractedWords(sortChecked ? words.sort((a,b)=>a.localeCompare(b,'ko')): words);
+ const uniqueSet = new Set();
+ const result: string[] = [];
+ words.forEach((word)=>{
+ const cleanedWord = word.replace(/[.,!?;:()]/g, ''); // 특수문자 제거
+ if (cleanedWord && !uniqueSet.has(cleanedWord)) {
+ uniqueSet.add(cleanedWord);
+ result.push(cleanedWord);
+ }
+ })
+ setExtractedWords(sortChecked ? result.sort((a,b)=>a.localeCompare(b,'ko')): result);
}
} catch (err) {
handleError(err);
@@ -242,16 +251,16 @@ const WordExtractorApp = () => {
{/* Settings Card */}
-
+
-
-