Skip to content

Commit f263adf

Browse files
authored
Merge pull request #22 from StartUpLight/SRLT-42-맞춤법-검사-연동
[SRLT-42] 맞춤법 검사 연동
2 parents dae57dd + f719fe2 commit f263adf

File tree

15 files changed

+947
-296
lines changed

15 files changed

+947
-296
lines changed

src/api/business.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
11
import api from './api';
2-
import { BusinessPlanCreateResponse, BusinessPlanSubsectionRequest, BusinessPlanSubsectionResponse, SubSectionType } from '@/types/business/business.type';
2+
import {
3+
BusinessPlanCreateResponse,
4+
BusinessPlanSubsectionRequest,
5+
BusinessPlanSubsectionResponse,
6+
BusinessSpellCheckRequest,
7+
BusinessSpellCheckResponse,
8+
SubSectionType,
9+
} from '@/types/business/business.type';
310

411
export async function postBusinessPlan(): Promise<BusinessPlanCreateResponse> {
5-
const res = await api.post(`/v1/business-plans`);
6-
return res.data as BusinessPlanCreateResponse;
12+
const res = await api.post(`/v1/business-plans`);
13+
return res.data as BusinessPlanCreateResponse;
714
}
815

916
export async function postBusinessPlanSubsections(
10-
planId: number,
11-
body: BusinessPlanSubsectionRequest
17+
planId: number,
18+
body: BusinessPlanSubsectionRequest
1219
) {
13-
const res = await api.post(`/v1/business-plans/${planId}/subsections`, body);
14-
return res.data;
20+
const res = await api.post(`/v1/business-plans/${planId}/subsections`, body);
21+
return res.data;
1522
}
1623

1724
export async function getBusinessPlanSubsection(
18-
planId: number,
19-
subSectionType: SubSectionType
25+
planId: number,
26+
subSectionType: SubSectionType
2027
): Promise<BusinessPlanSubsectionResponse> {
21-
const res = await api.get(`/v1/business-plans/${planId}/subsections/${subSectionType}`);
22-
return res.data as BusinessPlanSubsectionResponse;
28+
const res = await api.get(
29+
`/v1/business-plans/${planId}/subsections/${subSectionType}`
30+
);
31+
return res.data as BusinessPlanSubsectionResponse;
2332
}
2433

34+
export async function postSpellCheck(body: BusinessSpellCheckRequest) {
35+
const res = await api.post<BusinessSpellCheckResponse>(
36+
`/v1/business-plans/spellcheck`,
37+
body
38+
);
2539

40+
return res.data;
41+
}

src/app/business/components/SpellCheck.tsx

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,67 @@
11
'use client';
2-
import React, { useState } from 'react';
2+
import React, { useEffect, useMemo, useState } from 'react';
33
import Button from '@/app/_components/common/Button';
44
import Arrow from '@/assets/icons/arrow_up.svg';
5+
import { useSpellCheckStore } from '@/store/spellcheck.store';
6+
import { useEditorStore } from '@/store/editor.store';
7+
import { applyAllCorrections, type CorrectionPair } from '@/util/spellReplace';
8+
import { applySpellHighlights } from '@/util/spellMark';
9+
import type { Editor } from '@tiptap/core';
10+
11+
type UIResult = {
12+
id: number;
13+
original: string;
14+
corrected: string;
15+
open: boolean;
16+
custom: string;
17+
};
518

619
const SpellCheck = () => {
20+
const { items, loading } = useSpellCheckStore();
721
const [isOpen, setIsOpen] = useState(false);
22+
const [results, setResults] = useState<UIResult[]>([]);
823

9-
const [results, setResults] = useState([
10-
{
11-
id: 1,
12-
original: '안농하세요',
13-
corrected: '안녕하세요',
14-
open: false,
15-
custom: '',
16-
},
17-
{
18-
id: 2,
19-
original: '안농하세요',
20-
corrected: '안녕하세요',
21-
open: false,
22-
custom: '',
23-
},
24-
{
25-
id: 3,
26-
original: '안농하세요',
27-
corrected: '안녕하세요',
28-
open: false,
29-
custom: '',
30-
},
31-
{
32-
id: 4,
33-
original: '안농하세요',
34-
corrected: '안녕하세요',
35-
open: false,
24+
const { features, skills, goals, sectionNumber } = useEditorStore();
25+
const editors = useMemo(
26+
() =>
27+
(sectionNumber === '0' ? [features, skills, goals] : [features]).filter(
28+
(e): e is Editor => !!e && !e.isDestroyed
29+
),
30+
[features, skills, goals, sectionNumber]
31+
);
32+
33+
const [pendingPairs, setPendingPairs] = useState<CorrectionPair[] | null>(
34+
null
35+
);
36+
37+
useEffect(() => {
38+
if (loading) setIsOpen(true);
39+
}, [loading]);
40+
41+
useEffect(() => {
42+
const next = (items ?? []).map((item) => ({
43+
id: item.id,
44+
original: item.original ?? '',
45+
corrected:
46+
(Array.isArray(item.suggestions) && item.suggestions[0]) ||
47+
item.corrected ||
48+
item.original ||
49+
'',
50+
open: Boolean(item.open),
3651
custom: '',
37-
},
38-
]);
52+
}));
53+
setResults(next);
54+
}, [items]);
55+
56+
useEffect(() => {
57+
if (!pendingPairs || editors.length === 0) return;
58+
const raf = requestAnimationFrame(() => {
59+
applyAllCorrections(editors, pendingPairs);
60+
applySpellHighlights(editors, items);
61+
setPendingPairs(null);
62+
});
63+
return () => cancelAnimationFrame(raf);
64+
}, [pendingPairs, editors, items]);
3965

4066
const toggleItem = (id: number) => {
4167
setResults((prev) =>
@@ -52,21 +78,49 @@ const SpellCheck = () => {
5278
};
5379

5480
const handleApply = (id: number) => {
55-
setResults((prev) =>
56-
prev.map((item) =>
81+
setResults((prev) => {
82+
const target = prev.find((p) => p.id === id);
83+
if (target) {
84+
const replacement =
85+
target.custom || target.corrected || target.original;
86+
setPendingPairs([
87+
{ original: target.original, corrected: replacement },
88+
]);
89+
}
90+
return prev.map((item) =>
5791
item.id === id
58-
? { ...item, corrected: item.custom || item.corrected, open: false }
92+
? {
93+
...item,
94+
corrected: item.custom || item.corrected,
95+
open: false,
96+
custom: '',
97+
}
5998
: item
60-
)
99+
);
100+
});
101+
};
102+
103+
const handleApplyAll = () => {
104+
const pairs: CorrectionPair[] = results.map((r) => ({
105+
original: r.original,
106+
corrected: r.custom || r.corrected || r.original,
107+
}));
108+
setPendingPairs(pairs);
109+
setResults((prev) =>
110+
prev.map((it) => ({ ...it, open: false, custom: '' }))
61111
);
62112
};
63113

64114
return (
65115
<div
66-
className={`flex w-full flex-col rounded-[12px] bg-white ${isOpen ? 'h-[297px]' : ''}`}
116+
className={`flex w-full flex-col rounded-[12px] bg-white ${
117+
isOpen ? 'h-[297px]' : ''
118+
}`}
67119
>
68120
<div
69-
className={`flex w-full items-center justify-between border-b border-gray-200 px-6 ${isOpen ? 'pt-4 pb-[10px]' : 'py-4'}`}
121+
className={`flex w-full items-center justify-between border-b border-gray-200 px-6 ${
122+
isOpen ? 'pt-4 pb-[10px]' : 'py-4'
123+
}`}
70124
>
71125
<span className="ds-subtitle font-semibold text-gray-900">
72126
맞춤법 검사
@@ -75,6 +129,8 @@ const SpellCheck = () => {
75129
type="button"
76130
aria-label="맞춤법검사 토글"
77131
onClick={() => setIsOpen((prev) => !prev)}
132+
disabled={loading}
133+
className={loading ? 'cursor-not-allowed opacity-60' : ''}
78134
>
79135
<Arrow
80136
className={`cursor-pointer ${isOpen ? 'rotate-180' : 'rotate-0'}`}
@@ -135,7 +191,11 @@ const SpellCheck = () => {
135191
</div>
136192

137193
<div className="px-6 py-4">
138-
<Button text="모두 수정하기" className="w-full rounded-[8px]" />
194+
<Button
195+
text="모두 수정하기"
196+
className="w-full rounded-[8px]"
197+
onClick={handleApplyAll}
198+
/>
139199
</div>
140200
</>
141201
)}

0 commit comments

Comments
 (0)