Skip to content

Commit a993812

Browse files
authored
Merge pull request Next-Room#85 from Next-Room/feat/hint-qa4
2 parents 90bb0ed + 2ab8bdd commit a993812

File tree

19 files changed

+262
-65
lines changed

19 files changed

+262
-65
lines changed

.eslintrc.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
}
3636
],
3737
"react/display-name": "off",
38+
"jsx-a11y/alt-text": "off",
39+
"@next/next/no-img-element": "off",
3840
"no-console": ["error", { "allow": ["warn", "error"] }]
3941
}
4042
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import "./textArea.modules.sass";
3+
import classNames from "classnames";
4+
5+
import useTextArea from "./useTextArea";
6+
import { ThemeInfoTextAreaType } from "./TextAreaType";
7+
8+
export default function ThemeTextArea({
9+
id,
10+
tabIndex,
11+
content,
12+
infoText,
13+
textAreaPlaceholder,
14+
checkErrorText,
15+
}: ThemeInfoTextAreaType) {
16+
const {
17+
textAreaValue,
18+
isFocus,
19+
setIsFocus,
20+
errorText,
21+
textAreaRef,
22+
handleTextAreaChange,
23+
handleTextAreaBlur,
24+
} = useTextArea({ id, content, checkErrorText });
25+
26+
return (
27+
<div tabIndex={isFocus ? -1 : tabIndex} onFocus={() => setIsFocus(true)}>
28+
<textarea
29+
ref={textAreaRef}
30+
className={classNames("theme-textarea", {
31+
error: errorText,
32+
filled: textAreaValue && !(errorText || isFocus),
33+
})}
34+
value={textAreaValue}
35+
placeholder={textAreaPlaceholder}
36+
onChange={handleTextAreaChange}
37+
onBlur={handleTextAreaBlur}
38+
tabIndex={tabIndex}
39+
/>
40+
41+
{errorText && (
42+
<div className="theme-textfield-info error" tabIndex={-1}>
43+
{errorText}
44+
</div>
45+
)}
46+
{infoText && (
47+
<div className="theme-textfield-info" tabIndex={-1}>
48+
{infoText}
49+
</div>
50+
)}
51+
</div>
52+
);
53+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type ValidationFunction<T> = (value: T) => string;
2+
3+
export type ThemeInfoTextAreaType = {
4+
id: "contents" | "answer";
5+
tabIndex?: number;
6+
title?: string;
7+
content: string;
8+
infoText?: string;
9+
textAreaPlaceholder?: string;
10+
checkErrorText?: ValidationFunction<unknown>;
11+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@import '../../style/variables'
2+
@import '../../style/mixins'
3+
@import '../../style/button'
4+
5+
.theme-textarea
6+
width: 100%
7+
min-height: 120px
8+
padding: 8px 12px
9+
display: flex
10+
border: 1px solid $color-white20
11+
border-radius: 8px
12+
background-color: $color-white5
13+
color: $color-white
14+
resize: none
15+
cursor: pointer
16+
17+
&.filled
18+
background-color: $color-main
19+
&:focus
20+
outline: none
21+
border: 1px solid $color-white
22+
background-color: $color-black
23+
cursor: text
24+
&:hover
25+
background-color: $color-black
26+
&.error
27+
border: 1px solid $color-semantic100
28+
cursor: text
29+
30+
.theme-textfield-info
31+
margin: 4px 0 0 16px
32+
cursor: default
33+
34+
@include body12R
35+
color: $color-white70
36+
&.error
37+
color: $color-semantic100
38+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ChangeEvent, FocusEvent, useEffect, useRef, useState } from "react";
2+
3+
import { useCreateTheme } from "@/components/atoms/createTheme.atom";
4+
import { useCreateHint } from "@/components/atoms/createHint.atom";
5+
6+
import { ThemeInfoTextAreaType } from "./TextAreaType";
7+
8+
const useTextArea = ({
9+
id,
10+
content,
11+
checkErrorText,
12+
}: ThemeInfoTextAreaType) => {
13+
const [textAreaValue, setTextAreaValue] = useState<string>(content || "");
14+
const [isFocus, setIsFocus] = useState<boolean>(false);
15+
const [errorText, setErrorText] = useState<string>("");
16+
const textAreaRef = useRef<HTMLTextAreaElement>(null);
17+
const [, setCreateHint] = useCreateHint();
18+
const [, setCreateTheme] = useCreateTheme();
19+
20+
useEffect(() => {
21+
if (errorText) return;
22+
setCreateTheme((prev) => ({
23+
...prev,
24+
[id]: textAreaValue,
25+
}));
26+
setCreateHint((prev) => ({
27+
...prev,
28+
[id]: textAreaValue,
29+
}));
30+
}, [textAreaValue, id, setCreateTheme, setCreateHint, errorText]);
31+
32+
useEffect(() => {
33+
if (!isFocus || !textAreaRef.current) {
34+
setErrorText("");
35+
return;
36+
}
37+
textAreaRef.current.focus();
38+
}, [isFocus]);
39+
40+
const handleTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
41+
const cur = e.target.value;
42+
const error = checkErrorText ? checkErrorText(cur) : "";
43+
if (error) {
44+
setErrorText(error);
45+
setTextAreaValue(textAreaValue);
46+
return;
47+
}
48+
setErrorText("");
49+
setTextAreaValue(cur);
50+
};
51+
52+
const handleTextAreaBlur = (e: FocusEvent<HTMLTextAreaElement>) => {
53+
if (
54+
!e.relatedTarget ||
55+
(e.relatedTarget.className !== "theme-info focus" &&
56+
e.relatedTarget.className !== "theme-info error")
57+
) {
58+
setIsFocus(false);
59+
return;
60+
}
61+
textAreaRef.current?.focus();
62+
setIsFocus(true);
63+
};
64+
65+
return {
66+
textAreaValue,
67+
isFocus,
68+
setIsFocus,
69+
errorText,
70+
textAreaRef,
71+
handleTextAreaChange,
72+
handleTextAreaBlur,
73+
};
74+
};
75+
76+
export default useTextArea;

app/admin/(components)/Sidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default function Sidebar(props: Props) {
6262
const navigateToNewTheme = () => {
6363
resetSelectedTheme();
6464
router.push("/admin");
65+
setDrawer({ ...drawer, isOpen: false });
6566
};
6667
const handleSelectTheme = (theme: Theme) => {
6768
if (drawer.isOpen && !drawer.isSameHint) {
@@ -134,7 +135,7 @@ export default function Sidebar(props: Props) {
134135
</button>
135136
</li>
136137
</ul>
137-
{!status?.includes("SUBSCRIPTION") && (
138+
{!(status?.replaceAll(`"`, "") === "SUBSCRIPTION") && (
138139
<div className="sidebar__subscribe">
139140
<p className="sidebar__subscribe-title">
140141
구독하고 힌트에 사진을 추가해 보세요

app/admin/(components)/ThemeDrawer/Container.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import Image from "next/image";
22

33
import { useSelectedHint } from "@/components/atoms/selectedHint.atom";
44
import ThemeTextField from "@/(shared)/(ThemeTextField)/Container";
5+
import ThemeTextArea from "@/(shared)/(ThemeTextArea)/Container";
56

67
import ThemeDrawerAnswer from "./ThemeDrawerAnswer";
78
import ThemeDrawerHint from "./ThemeDrawerHint";
89
import {
10+
answerTextAreaProps,
911
codeTextFieldProps,
1012
HintImageProps,
13+
hintTextAreaProps,
1114
rateTextFieldProps,
1215
XImageProps,
1316
} from "./consts/themeDrawerProps";
@@ -81,12 +84,20 @@ const ThemeDrawer = ({ hintType, handleHintCreate }: DrawerType) => {
8184
images={hintImages}
8285
setImages={setHintImages}
8386
/>
87+
<ThemeTextArea
88+
{...hintTextAreaProps}
89+
content={selectedHint.contents ? selectedHint.contents : ""}
90+
/>
8491

8592
<ThemeDrawerAnswer
8693
imageType={"answer"}
8794
images={answerImages}
8895
setImages={setAnswerImages}
8996
/>
97+
<ThemeTextArea
98+
{...answerTextAreaProps}
99+
content={selectedHint.answer ? selectedHint.answer : ""}
100+
/>
90101
</div>
91102

92103
{hintType === "Edit" ? (

app/admin/(components)/ThemeDrawer/ThemeDrawerAnswer.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const ThemeDrawerAnswer = ({
1919
handleFileInputClick,
2020
handleFileInputChange,
2121
handleAddImageBtnClick,
22-
handleTextAreaChange,
2322
deleteLocalImage,
2423
deleteServerImage,
2524
answerInputRef,
@@ -53,9 +52,9 @@ const ThemeDrawerAnswer = ({
5352
}/3)`}
5453
</button>
5554
</div>
56-
<div className="drawer-images">
57-
{selectedHint?.answerImageUrlList?.map((src, idx) => (
58-
<div className="drawer-image-box" key={src}>
55+
{selectedHint?.answerImageUrlList?.map((src, idx) => (
56+
<div className="drawer-images" key={src}>
57+
<div className="drawer-image-box">
5958
<img src={src} alt={`answer-preview-${src}`} />
6059
<div
6160
className="drawer-image-dimmed"
@@ -66,10 +65,12 @@ const ThemeDrawerAnswer = ({
6665
</button>
6766
</div>
6867
</div>
69-
))}
70-
{images.length > 0 &&
71-
images.map((file, index) => (
72-
<div key={file.name} className="drawer-image-box">
68+
</div>
69+
))}
70+
{images.length > 0 &&
71+
images.map((file, index) => (
72+
<div className="drawer-images" key={file.name}>
73+
<div className="drawer-image-box">
7374
<img
7475
src={URL.createObjectURL(file)}
7576
alt={`answer-preview-${index}`}
@@ -83,15 +84,8 @@ const ThemeDrawerAnswer = ({
8384
</button>
8485
</div>
8586
</div>
86-
))}
87-
</div>
88-
89-
<textarea
90-
className="drawer-content-textarea"
91-
placeholder="정답 내용을 입력해 주세요."
92-
onChange={handleTextAreaChange}
93-
defaultValue={selectedHint.answer}
94-
/>
87+
</div>
88+
))}
9589
</div>
9690
);
9791
};

app/admin/(components)/ThemeDrawer/ThemeDrawerHint.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const ThemeDrawerHint = ({
2020
handleFileInputClick,
2121
handleFileInputChange,
2222
handleAddImageBtnClick,
23-
handleTextAreaChange,
2423
deleteLocalImage,
2524
deleteServerImage,
2625
hintInputRef,
@@ -53,9 +52,9 @@ const ThemeDrawerHint = ({
5352
}/3)`}
5453
</button>
5554
</div>
56-
<div className="drawer-images">
57-
{selectedHint?.hintImageUrlList?.map((src, idx) => (
58-
<div className="drawer-image-box" key={src}>
55+
{selectedHint?.hintImageUrlList?.map((src, idx) => (
56+
<div className="drawer-images" key={src}>
57+
<div className="drawer-image-box">
5958
<img src={src} alt={`hint-preview-${src}`} />
6059
<div
6160
className="drawer-image-dimmed"
@@ -66,10 +65,12 @@ const ThemeDrawerHint = ({
6665
</button>
6766
</div>
6867
</div>
69-
))}
70-
{images.length > 0 &&
71-
images.map((file, index) => (
72-
<div key={file.name} className="drawer-image-box">
68+
</div>
69+
))}
70+
{images.length > 0 &&
71+
images.map((file, index) => (
72+
<div className="drawer-images" key={file.name}>
73+
<div className="drawer-image-box">
7374
<img
7475
src={URL.createObjectURL(file)}
7576
alt={`hint-preview-${index}`}
@@ -83,15 +84,8 @@ const ThemeDrawerHint = ({
8384
</button>
8485
</div>
8586
</div>
86-
))}
87-
</div>
88-
89-
<textarea
90-
className="drawer-content-textarea"
91-
placeholder="힌트 내용을 입력해 주세요."
92-
onChange={handleTextAreaChange}
93-
defaultValue={selectedHint.contents}
94-
/>
87+
</div>
88+
))}
9589
</div>
9690
);
9791
};

app/admin/(components)/ThemeDrawer/consts/themeDrawerProps.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ThemeInfoTextFieldType } from "@/(shared)/(ThemeTextField)/TextFieldType";
2+
import { ThemeInfoTextAreaType } from "@/(shared)/(ThemeTextArea)/TextAreaType";
23

34
import {
45
codeValidations,
@@ -18,7 +19,7 @@ export const codeTextFieldProps: ThemeInfoTextFieldType = {
1819

1920
export const rateTextFieldProps: ThemeInfoTextFieldType = {
2021
id: "progress",
21-
tabIndex: 1,
22+
tabIndex: 2,
2223
title: "문제 진행률(%)",
2324
content: "",
2425
infoText: "",
@@ -27,6 +28,22 @@ export const rateTextFieldProps: ThemeInfoTextFieldType = {
2728
checkErrorText: progressValidations,
2829
};
2930

31+
export const hintTextAreaProps: ThemeInfoTextAreaType = {
32+
id: "contents",
33+
tabIndex: 3,
34+
content: "",
35+
infoText: "",
36+
textAreaPlaceholder: "힌트 내용을 입력해 주세요.",
37+
};
38+
39+
export const answerTextAreaProps: ThemeInfoTextAreaType = {
40+
id: "answer",
41+
tabIndex: 4,
42+
content: "",
43+
infoText: "",
44+
textAreaPlaceholder: "정답 내용을 입력해 주세요.",
45+
};
46+
3047
export const XImageProps = {
3148
src: "/images/svg/icon_X.svg",
3249
alt: "x_button",

0 commit comments

Comments
 (0)