Commit e7ae6d3f authored by Yoon, Daeki's avatar Yoon, Daeki 😅
Browse files

verion 0.1 시작

parent 1ea0c555
import React from "react";
export const Preview = () => {
return <div>Preview</div>;
};
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { surveyApi } from "../apis";
import { catchErrors } from "../helpers";
import { ISurvey } from "../types";
import { SurveyCard } from "./SurveyCard";
export const Profile = () => {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [surveys, setSurveys] = useState<ISurvey[]>([]);
useEffect(() => {
getSurveys();
}, []);
async function getSurveys() {
const surveys: ISurvey[] = await surveyApi.getSurveys();
// console.log(surveys);
setSurveys(surveys);
}
async function deleteSurvey(id: string) {
if (window.confirm("해당 설문조사를 삭제하시겠습니까?")) {
try {
setLoading(true);
const result = await surveyApi.deleteSurvey(id);
console.log("deleted survey", result);
setError("");
const newItems = surveys.filter((survey) => survey._id !== result._id);
// console.log("items left:", newItems);
setSurveys(newItems);
alert("삭제되었습니다.");
} catch (error) {
console.log("에러발생");
catchErrors(error, setError);
} finally {
setLoading(false);
}
}
}
return (
<div className="flex flex-col items-center">
<div className="mt-10 text-xl font-bold">나의 설문조사</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-6">
<Link
to={"/surveys/create"}
className="flex w-40 h-48 md:h-60 md:w-52 items-center font-bold bg-gray-200 hover:bg-themeColor rounded-lg "
>
<div className="text-center md:px-6 md:py-6 font-xs md:font-bold text-gray-500 place-items-center hover:text-white">
CREATE NEW SURVEY!
</div>
</Link>
{surveys.map((survey) => {
return (
<SurveyCard
survey={survey}
key={survey._id}
handleDelete={deleteSurvey}
/>
);
})}
</div>
</div>
);
};
import React, { useState } from "react";
import { getEnumKeyByEnumValue, QUESTION_TYPES } from "../commons";
import { getElementByQuestionType } from "../helpers";
import { IQuestionProps } from "../types";
const options = Object.entries(QUESTION_TYPES).map(([type, value]) => (
<option key={type} value={value}>
{value}
</option>
));
export const Question = ({
element,
handleQuestion,
deleteQuestion,
}: IQuestionProps) => {
const [question, setQuestion] = useState(element);
const isEditing = question.isEditing;
async function handleEditComplete() {
question.isEditing = false;
console.log("editing completed:", question);
handleQuestion(question);
}
function handleSelect(event: React.ChangeEvent<HTMLSelectElement>) {
const selectedType = event.currentTarget.value;
console.log(selectedType);
const selectedKind =
getEnumKeyByEnumValue(QUESTION_TYPES, selectedType) ?? "singletext";
console.log("selected kind:", selectedKind);
setQuestion({ ...question, type: selectedKind });
}
const handleElement = () => {
console.log("handle element");
setQuestion({ ...question });
};
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { checked, name, value } = event.currentTarget;
if (name === "isRequired") {
return setQuestion({ ...question, [name]: checked });
}
setQuestion({ ...question, [name]: value });
}
const onCancel = () => {
const originalQuestion = { ...element, isEditing: false };
setQuestion(originalQuestion);
handleQuestion(originalQuestion);
};
const onDelete = () => {
if (window.confirm("질문을 삭제하시겠습니까?")) {
deleteQuestion(question._id);
}
};
const onEdit = () => {
setQuestion({ ...question, isEditing: true });
handleQuestion({ ...question, isEditing: true });
};
return (
<div
style={{ borderColor: isEditing ? "red" : "#0A8A8A" }}
className="flex flex-col container w-4/5 h-auto border-2 items-center m-3 py-2 rounded-lg"
>
<div className="flex h-16 w-full place-content-center items-center">
<input
type="text"
name="title"
id={question._id}
className="text-xl font-bold border-b-2 w-11/12"
placeholder={"Question Title"}
value={question.title}
onChange={handleChange}
disabled={!isEditing}
></input>
</div>
<div className="flex w-full justify-center">
<input
type="text"
name="comment"
id={question._id}
className="border w-11/12"
placeholder="질문에 대한 설명을 입력해주세요"
value={question.comment}
onChange={handleChange}
disabled={!isEditing}
></input>
</div>
{getElementByQuestionType(question, handleElement, isEditing)}
<div className="flex flex-row place-content-between w-11/12 py-2">
<select
id={question._id}
name="type"
onChange={handleSelect}
disabled={!isEditing}
value={QUESTION_TYPES[question.type]}
className="w-32 h-10 md:w-36 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-themeColor"
>
{options}
</select>
<div className="place-self-end py-2">
<input
type="checkbox"
id="isRequired"
name="isRequired"
onChange={handleChange}
disabled={!isEditing}
checked={question.isRequired}
/>
<label htmlFor="isRequired" className="px-1">
필수
</label>
{isEditing ? (
<>
<button type="button" className="px-1" onClick={onCancel}>
취소
</button>
<button
type="button"
className="px-1"
onClick={handleEditComplete}
>
확인
</button>
</>
) : (
<>
<button type="button" className="px-1" onClick={onDelete}>
삭제
</button>
<button type="button" className="px-1" onClick={onEdit}>
수정
</button>
</>
)}
</div>
</div>
</div>
);
};
import React from "react";
import { CreateQuestionData } from "../types";
import { Question } from "./Question";
type Props = {
questions: CreateQuestionData[];
handleQuestion: Function;
deleteQuestion: Function;
};
export const QuestionsList = ({
questions,
handleQuestion,
deleteQuestion,
}: Props) => {
return (
<>
{questions.map((question) => (
<Question
key={question._id}
element={question}
handleQuestion={handleQuestion}
deleteQuestion={deleteQuestion}
/>
))}
</>
);
};
import React from "react";
import { Link } from "react-router-dom";
import { ISurvey } from "../types";
import { DuplicateIcon } from "../icons";
type Props = {
survey: ISurvey;
handleDelete: (id: string) => Promise<void>;
};
export const SurveyCard = ({ survey, handleDelete }: Props) => {
const copyLink = async () => {
await navigator.clipboard.writeText(
`http://localhost:8080/answers/${survey._id}`
);
alert("설문조사의 링크가 클립보드에 저장되었습니다.");
};
const onDelete = async () => {
survey._id && (await handleDelete(survey._id));
};
return (
<div className="w-40 h-48 md:w-52 md:h-60 rounded border-2 hover:border-2 hover:border-themeColor">
<Link
to={`/surveys/${survey._id}/edit`}
state={survey}
className="w-full pt-1"
>
<p className="font-bold">
{survey.title ? survey.title : "제목없는 설문조사"}
</p>
<div className="h-24 md:h-36 p-3 overflow-y-hidden hover:overflow-y-auto">
<p className="text-gray-700 text-justify">
{survey.comment ? survey.comment : "설명없는 설문조사"}
</p>
</div>
<p className="text-gray-500 text-sm">
{survey.updatedAt?.substring(0, 10)}
</p>
</Link>
<div className="flex justify-end pt-1 pr-1">
<button className="flex place-self-center" onClick={copyLink}>
링크복사
<DuplicateIcon className="w-7 h-7" />
</button>
<button
type="button"
className="bg-themeColor rounded text-white py-1 px-1.5 ml-1 mr-1.5"
onClick={onDelete}
>
삭제
</button>
</div>
</div>
);
};
export { AnswerSurvey } from "./AnswerSurvey";
export { CreateSurvey } from "./CreateSurvey";
export { EditSurvey } from "./EditSurvey";
export { Preview } from "./Preview";
export { Profile } from "./Profile";
......@@ -10,44 +10,51 @@ export interface SignupUser {
password: string;
}
export interface SurveyType {
export interface ISurvey {
_id: string;
user: any;
title: string;
comment: string;
questions: BasicQuestionType[];
questions: IQuestionData[];
createdAt?: string;
updatedAt?: string;
}
export interface BasicQuestionType {
interface IChoice {
value: number;
text: string;
}
interface IBasicContent {
choices: IChoice[];
[key: string]: any;
}
export interface IQuestionData {
_id?: string;
order: number;
type: string;
_id: string;
title: string;
isRequired: boolean;
comment: string;
content: any;
answers?: any;
content: IBasicContent;
// answers?: any;
[key: string]: string | number | boolean | any;
}
export interface AnswerQuestionType extends BasicQuestionType {
export interface AnswerQuestionType extends IQuestionData {
requiredCheck: boolean;
answer: any;
}
export interface AnswerSurveyType extends SurveyType {
export interface AnswerSurveyType extends ISurvey {
questions: AnswerQuestionType[];
}
export interface EssayType extends BasicQuestionType {}
export interface DateType extends BasicQuestionType {}
export interface RadioType extends BasicQuestionType {
content: {
choices: {
value: number;
text: string;
}[];
export interface IEssay extends IQuestionData {}
export interface IDate extends IQuestionData {}
export interface IRadio extends IQuestionData {
content: IBasicContent & {
hasOther: boolean;
otherText: string;
};
......@@ -58,33 +65,23 @@ interface IChoices {
text: string;
}
export interface CheckboxType extends BasicQuestionType {
content: {
choices: IChoices[];
maxCount: number;
};
export interface ICheckbox extends IQuestionData {
content: IBasicContent & { maxCount: number };
}
export interface DropdownType extends BasicQuestionType {
content: {
choices: IChoices[];
hasNone: boolean;
};
export interface IDropdown extends IQuestionData {
content: IBasicContent & { hasNone: boolean };
}
export interface FileType extends BasicQuestionType {
content: {
export interface IFile extends IQuestionData {
content: IBasicContent & {
filename: string;
value: string;
};
}
export interface RatingType extends BasicQuestionType {
content: {
choices: {
value: number;
text: string;
}[];
export interface IRating extends IQuestionData {
content: IBasicContent & {
minRateDescription: string;
maxRateDescription: string;
};
......@@ -103,7 +100,7 @@ export interface AnswerType {
}
export interface AnswerProps {
element: BasicQuestionType;
element: IQuestionData;
answerQuestion: AnswerQuestionType;
// answers: AnswersType | undefined;
// handleAnswer: () => void;
......
import { NextFunction, Request, Response } from "express";
import { Types } from "mongoose";
import { surveyDb } from "../db";
import { asyncWrap } from "../helpers/asyncWrap";
import { ISurvey } from "../models";
export interface TypedRequestAuth<T> extends Request {
auth: T;
......@@ -11,10 +13,9 @@ export const createSurvey = asyncWrap(
async (reqExp: Request, res: Response) => {
const req = reqExp as TypedRequestAuth<{ userId: string }>;
const { userId } = req.auth;
let survey = req.body;
survey.user = userId;
let survey = req.body as ISurvey;
survey.user = new Types.ObjectId(userId);
console.log("survey body", survey);
delete survey._id;
const newSurvey = await surveyDb.createSurvey(survey);
return res.json(newSurvey);
}
......
import { Survey, ISurvey } from "../models";
import { HydratedDocument } from "mongoose";
import { Survey, ISurvey, Question, IQuestion } from "../models";
export const findUserBySurveyId = async (surveyId: string) => {
const survey = await Survey.findById(surveyId).populate("user");
......@@ -10,8 +11,24 @@ export const findUserBySurveyId = async (surveyId: string) => {
return null;
};
export const createSurvey = async (survey: ISurvey) => {
const newSurvey = await Survey.create(survey);
export const createSurvey = async (surveyData: ISurvey) => {
const { _id, questions, ...rest } = surveyData;
console.log("questions in survey db:", questions, "rest:", rest);
let newQuestions;
// questions 있으면 먼저 저장
if (questions && questions.length > 0) {
newQuestions = await Promise.all(
questions.map(async (question) => {
const { _id, ...questionsWithoutId } = question;
return await Question.create(questionsWithoutId);
})
);
}
const survey = new Survey({
...rest,
questions: newQuestions,
});
const newSurvey = await (await survey.save()).populate("questions");
return newSurvey;
};
......@@ -22,11 +39,13 @@ export const getSurveyById = async (surveyId: string) => {
};
export const getSurveys = async (userId: string) => {
const surveys = await Survey.find({ user: userId }).sort({ updatedAt: -1 });
const surveys = await Survey.find({ user: userId })
.sort({ updatedAt: -1 })
.populate("questions");
return surveys;
};
export const updateSurvey = async (survey: ISurvey) => {
export const updateSurvey = async (survey: HydratedDocument<ISurvey>) => {
const newSurvey = await Survey.findOneAndUpdate({ _id: survey._id }, survey);
return newSurvey;
};
......
......@@ -3,18 +3,15 @@ import { authCtrl, surveyCtrl, questionCtrl } from "../controllers";
const router = express.Router();
router.route("/").get(authCtrl.requireLogin, surveyCtrl.getSurveys);
router.route("/create").post(authCtrl.requireLogin, surveyCtrl.createSurvey);
router.route("/:surveyId").get(surveyCtrl.getSurveyById);
router
.route("/")
.get(authCtrl.requireLogin, surveyCtrl.getSurveys)
.post(authCtrl.requireLogin, surveyCtrl.createSurvey);
router
.route("/:surveyId/edit")
.route("/:surveyId")
.get(authCtrl.requireLogin, authCtrl.authenticate, surveyCtrl.getSurveyById)
.put(authCtrl.requireLogin, authCtrl.authenticate, surveyCtrl.updateSurvey);
router
.route("/:surveyId/delete")
.put(authCtrl.requireLogin, authCtrl.authenticate, surveyCtrl.updateSurvey)
.delete(
authCtrl.requireLogin,
authCtrl.authenticate,
......@@ -23,7 +20,11 @@ router
router
.route("/:surveyId/questions")
.post(authCtrl.requireLogin, questionCtrl.createQuestion);
.post(
authCtrl.requireLogin,
authCtrl.authenticate,
questionCtrl.createQuestion
);
router.param("surveyId", surveyCtrl.userBySurveyId);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment