Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 댓글, 관리자사전승인, 기업카드, 라디오버튼 #35

Merged
merged 11 commits into from
Aug 16, 2024
Binary file added public/images/lululabLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@import url("src/theme/fonts/pretendard.css");
/* import { Pretendard } from "./fonts"; 이렇게 쓰는거랑 무슨 차이지...?? 일케 써야되는건감*/

.tableContainer {
padding: 20px;
background-color: var(--color-surfaceContainerLowest); /* 배경색을 하얀색으로 설정 */
border-radius: 0; /* 모서리를 90도로 설정 */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
font-family: "Pretendard Variable", __Pretendard_Fallback_e223ab, sans-serif;
/* color: #000; */
}

.table {
width: 100%;
border-collapse: collapse;
}

.table th,
.table td {
padding: 10px; /* 행 간 간격을 유지 */
padding-left: 20px; /* 열 간격을 늘리기 위해 좌우 패딩 조정 */
padding-right: 20px; /* 열 간격을 늘리기 위해 좌우 패딩 조정 */
text-align: left;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.table th {
border-bottom: 2px solid var(--color-onBackground);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AdminSignupPreview } from "./AdminSignupPreview";

const meta: Meta<typeof AdminSignupPreview> = {
title: "AdminSignupPreview",
component: AdminSignupPreview,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
args: {},
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Usage: Story = {
args: {
signups: [
{ id: 1, applicant: "김교수", date: "2024/07/05", category: "교수", remark: "" },
{
id: 2,
applicant: "나공기업",
date: "2024/07/05",
category: "공공기관",
remark: "인공지능지원사업부",
},
{ id: 3, applicant: "김교수", date: "2024/07/05", category: "교수", remark: "" },
{
id: 4,
applicant: "나공기업",
date: "2024/07/05",
category: "공공기관",
remark: "인공지능지원사업부",
},
],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import { AdminSignupPreview } from "./AdminSignupPreview";
import "@testing-library/jest-dom";

describe("AdminSignupPreview component", () => {
it("renders correctly with given props", () => {
const signups = [
{ id: 1, applicant: "김교수", date: "2024/07/05", category: "교수", remark: "" },
{
id: 2,
applicant: "나공기업",
date: "2024/07/05",
category: "공공기관",
remark: "인공지능지원사업부",
},
{ id: 3, applicant: "김교수", date: "2024/07/05", category: "교수", remark: "" },
{
id: 4,
applicant: "나공기업",
date: "2024/07/05",
category: "공공기관",
remark: "인공지능지원사업부",
},
];

render(<AdminSignupPreview signups={signups} />);

expect(screen.getByText("ID")).toBeInTheDocument();
expect(screen.getByText("신청자")).toBeInTheDocument();
expect(screen.getByText("신청일")).toBeInTheDocument();
expect(screen.getByText("분류")).toBeInTheDocument();
expect(screen.getByText("비고")).toBeInTheDocument();

signups.forEach((signup) => {
expect(screen.getByText(signup.id)).toBeInTheDocument();
expect(screen.getByText(signup.applicant)).toBeInTheDocument();
expect(screen.getByText(signup.date)).toBeInTheDocument();
expect(screen.getByText(signup.category)).toBeInTheDocument();
if (signup.remark) {
expect(screen.getByText(signup.remark)).toBeInTheDocument();
}
});
});
});
43 changes: 43 additions & 0 deletions src/components/common/AdminSignupPreview/AdminSignupPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import classes from "./AdminSignupPreview.module.css";

interface Signup {
id: number;
applicant: string;
date: string;
category: string;
remark?: string;
}

interface AdminSignupPreviewProps {
signups: Signup[];
}

export const AdminSignupPreview: React.FC<AdminSignupPreviewProps> = ({ signups }) => {
return (
<div className={classes.tableContainer}>
<table className={classes.table}>
<thead>
<tr>
<th>ID</th>
<th>신청자</th>
<th>신청일</th>
<th>분류</th>
<th>비고</th>
</tr>
</thead>
<tbody>
{signups.map((signup) => (
<tr key={signup.id}>
<td>{signup.id}</td>
<td>{signup.applicant}</td>
<td>{signup.date}</td>
<td>{signup.category}</td>
<td>{signup.remark}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
88 changes: 88 additions & 0 deletions src/components/common/CommentBox/CommentBox.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
@import url("src/theme/fonts/pretendard.css");

.commentBoxContainer {
display: flex;
align-items: center;
width: 100%; /* 가로 길이를 100%로 설정 */
max-width: 600px; /* 최대 가로 길이를 설정하여 너무 길어지지 않도록 제한 */
padding: 10px;
background-color: var(--color-surfaceContainerLowest);
}

.textareaContainer {
display: flex;
border: 1.8px solid var(--color-surfaceVariant);
overflow: hidden;
width: 100%; /* 부모 요소의 너비에 맞게 설정 */
}

.verticalDivider {
border-left: 2px solid var(--color-surfaceVariant); /* 구분선 스타일 */
}

.textarea {
background-color: var(--color-surfaceContainerLowest);
flex: 1;
padding: 10px;
border: none;
resize: none;
outline: none;
font-family: inherit; /* 상속된 폰트 사용 */
width: 550px;
height: 70px;
font-size: 14px;
/* vw...? */
}

.button {
width: 70px;
background-color: var(--color-primary);
color: var(--color-onPrimary);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-family: inherit; /* 상속된 폰트 사용 */
}

.button:hover {
background-color: var(--color-onPrimaryFixedVariant);
}

.divider {
border-top: 2px solid #ccc; /* 굵기를 2px로 조정 */
margin: 10px 0; /* 간격을 줄임 */
}

.commentsList {
margin-top: 0px; /* 간격을 줄임 */
}

.commentItem {
border-top: 1px solid #ccc;
padding: 10px 0;
}

.commentItem:first-child {
border-top: 1px;
padding-top: 1px;
}

.commentAuthor {
color: #9f9f9f; /* 글씨 색상을 변경 */
margin-bottom: 5px;
font-weight: normal; /* 기본값으로 변경 */
font-size: 15px;
}

.commentContent {
font-size: 15px; /* 글씨 크기를 줄임 */
margin-top: 8px; /* 댓글 내용과 작성자 간의 간격을 추가 */
color: #5b5b5b;
white-space: pre-wrap; /* 줄바꿈을 유지 */
}

.commentTitle {
font-weight: bold; /* h3 태그 bold 처리 */
}
20 changes: 20 additions & 0 deletions src/components/common/CommentBox/CommentBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CommentBox } from "./CommentBox";

const meta: Meta<typeof CommentBox> = {
title: "CommentBox",
component: CommentBox,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
args: {},
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Usage: Story = {
args: {},
};
27 changes: 27 additions & 0 deletions src/components/common/CommentBox/CommentBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { CommentBox } from "./CommentBox";
import "@testing-library/jest-dom";

describe("CommentBox component", () => {
it("renders correctly", () => {
render(<CommentBox onSubmit={() => {}} />);
expect(screen.getByPlaceholderText("정책 위반 댓글은 삭제될 수 있습니다.")).toBeInTheDocument();
});

it("calls onSubmit with the correct value", () => {
const handleSubmit = jest.fn();
render(<CommentBox onSubmit={handleSubmit} />);
const textarea = screen.getByPlaceholderText("정책 위반 댓글은 삭제될 수 있습니다.");
fireEvent.change(textarea, { target: { value: "Test comment" } });
fireEvent.submit(textarea);
expect(handleSubmit).toHaveBeenCalledWith("Test comment");
});

it("displays submitted comment", () => {
render(<CommentBox />);
const textarea = screen.getByPlaceholderText("정책 위반 댓글은 삭제될 수 있습니다.");
fireEvent.change(textarea, { target: { value: "Test comment" } });
fireEvent.submit(textarea);
expect(screen.getByText("Test comment")).toBeInTheDocument();
});
});
66 changes: 66 additions & 0 deletions src/components/common/CommentBox/CommentBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import classes from "./CommentBox.module.css";

interface CommentBoxProps {
onSubmit?: (comment: string) => void;
}

interface Comment {
author: string;
content: string;
}

export const CommentBox: React.FC<CommentBoxProps> = ({ onSubmit }) => {
const [comment, setComment] = useState("");
const [comments, setComments] = useState<Comment[]>([]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.target.value);
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (comment.trim()) {
if (onSubmit) onSubmit(comment);
setComments([...comments, { author: "사람", content: comment }]);
setComment("");
}
};

return (
<div className={classes.commentBox}>
<h3 className={classes.commentTitle}>댓글</h3>
<form onSubmit={handleSubmit}>
<div className={classes.textareaContainer}>
<textarea
className={classes.textarea}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
Comment on lines +16 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

댓글에서 한글을 입력 후 엔터를 눌렀을 때, composition이 완료된 문자열(마지막 문자를 제외한 문자열)이 제출된 이후, 입력 중이었던 마지막 문자도 또 한 번 제출이 되어서, 총 2개의 댓글이 한 번에 입력되고 있습니다.
이 문제를 해결하기 위해서, textarea의 compositionstart, compositionend 이벤트를 활용하여 composition이 진행중인 경우 제출이 되지 않도록(composition이 끝난 경우에만 제출) 처리를 해주시면 좋을 것 같습니다.

자세한 내용은 IME composition 관련하여 찾아보시면 됩니다!

Suggested change
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.target.value);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (comment.trim()) {
if (onSubmit) onSubmit(comment);
setComments([...comments, { author: "사람", content: comment }]);
setComment("");
}
};
return (
<div className={classes.commentBox}>
<h3 className={classes.commentTitle}>댓글</h3>
<form onSubmit={handleSubmit}>
<div className={classes.textareaContainer}>
<textarea
className={classes.textarea}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
const [isComposing, setIsComposing] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.target.value);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey && !isComposing) {
e.preventDefault();
handleSubmit(e);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (comment.trim()) {
if (onSubmit) onSubmit(comment);
setComments([...comments, { author: "사람", content: comment }]);
setComment("");
}
};
return (
<div className={classes.commentBox}>
<h3 className={classes.commentTitle}>댓글</h3>
<form onSubmit={handleSubmit}>
<div className={classes.textareaContainer}>
<textarea
className={classes.textarea}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}

placeholder="정책 위반 댓글은 삭제될 수 있습니다."
/>
<div className={classes.verticalDivider}></div>
<button type="submit" className={classes.button}>
작성
</button>
</div>
</form>
<div className={classes.divider}></div>
<div className={classes.commentsList}>
{comments.map((comment, index) => (
<div key={index} className={classes.commentItem}>
<div className={classes.commentAuthor}>사람{index + 1}</div>
<div className={classes.commentContent}>{comment.content}</div>
</div>
))}
</div>
</div>
);
};
Loading
Loading