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

feature(admin): text 옵션 추가 #8

Open
wants to merge 2 commits into
base: feat/survey-web-submit
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 44 additions & 18 deletions apps/survey-admin/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import ToolBar from './components/ToolBar';
import SurveyItem from './components/SurveyItem';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import useSurveyViewModel from './hooks/viewmodel/useSurveyViewModel';
import { useEffect, useRef, useState } from 'react';

export default function App() {
const sectionsContainerRef = useRef<HTMLDivElement>(null);
const {
survey,
handleSurveyInput,
Expand All @@ -32,12 +34,30 @@ export default function App() {
handleAddSections,
handleDeleteItem,
onSubmit,
toolbarTop,
currentActiveItemIndex,
} = useSurveyViewModel();

const [toolbarTop, setToolbarTop] = useState(0);
const { sectionId, itemId } = currentActiveItemIndex;

useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout> | null = null;

const trackingActiveItemScroll = () => {
const el = sectionsContainerRef.current?.querySelector(
'div[data-active=true]'
) as HTMLDivElement;
const scroll = el.getBoundingClientRect().top + window.scrollY;
setToolbarTop(scroll);
timeoutId = setTimeout(trackingActiveItemScroll, 250);
};

timeoutId = setTimeout(trackingActiveItemScroll, 250);

return () => {
timeoutId && clearTimeout(timeoutId);
};
}, []);

return (
<div className={container}>
<div className={wrapper}>
Expand Down Expand Up @@ -67,32 +87,38 @@ export default function App() {
</div>
</Card>
<Block height={40} />
<div className={sectionsContainer}>
<div className={sectionsContainer} ref={sectionsContainerRef}>
{surveySections.map((section, sectionIndex) => (
<div key={section.id} className={sectionWrapper}>
<div className={sectionLabel}>{`${surveySections.length} 중 ${
sectionIndex + 1
} 섹션`}</div>
<div className={itemsContainer}>
{section.items.map((item, itemIndex) => (
<SurveyItem
<div
key={`${section.id}_${itemIndex}`}
isActive={
data-active={
sectionId === sectionIndex && itemId === itemIndex
}
item={item}
onActiveItem={(top) =>
handleActiveItem(sectionIndex, itemIndex, top)
}
onAddOptions={handleAddOption}
onChangeOptionText={handleChangeOptionText}
onChangeItemType={handleChangeItemType}
onChangeItemRequired={handleChangeItemRequired}
onChangeItemTitle={handleChangeItemTitle}
onDeleteItem={() =>
handleDeleteItem(sectionIndex, itemIndex)
}
/>
>
<SurveyItem
isActive={
sectionId === sectionIndex && itemId === itemIndex
}
item={item}
onActiveItem={() =>
handleActiveItem(sectionIndex, itemIndex)
}
onAddOptions={handleAddOption}
onChangeOptionText={handleChangeOptionText}
onChangeItemType={handleChangeItemType}
onChangeItemRequired={handleChangeItemRequired}
onChangeItemTitle={handleChangeItemTitle}
onDeleteItem={() =>
handleDeleteItem(sectionIndex, itemIndex)
}
/>
</div>
))}
</div>
</div>
Expand Down
23 changes: 14 additions & 9 deletions apps/survey-admin/src/app/components/SurveyItem/SurveyItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import {
} from './SurveyItem.css';
import { type SurveyItem } from '../../hooks/query/useServey';
import { useRef } from 'react';
import { itemsType } from '../../types/items.type';

interface SurveyItemProps {
item: SurveyItem;
isActive: boolean;
onActiveItem: (top: number) => void;
onActiveItem: () => void;
onAddOptions: () => void;
onChangeOptionText: (value: string, optionIndex: number) => void;
onChangeItemType: (type: 'checkbox' | 'radio' | 'select') => void;
onChangeItemType: (type: itemsType) => void;
onChangeItemTitle: (value: string) => void;
onChangeItemRequired: (isRequired: boolean) => void;
onDeleteItem: () => void;
Expand All @@ -47,9 +48,7 @@ const SurveyItem = ({
onAddOptions();
};
const handleActiveItem = () => {
if (!cardRef.current) return;
const { top } = cardRef.current.getBoundingClientRect();
onActiveItem(top + window.scrollY);
onActiveItem();
};
const handleItemsTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
Expand All @@ -64,7 +63,7 @@ const SurveyItem = ({
};
const handleChangeItemType = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.currentTarget;
onChangeItemType(value as 'radio' | 'select' | 'checkbox');
onChangeItemType(value as itemsType);
};
const handleItemRequiredChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { checked } = e.currentTarget;
Expand All @@ -91,16 +90,17 @@ const SurveyItem = ({
onChange={handleChangeItemType}
defaultValue={'radio'}
>
<option value="textarea">단답형</option>
<option value="radio">객관식</option>
<option value="checkbox">체크박스</option>
<option value="select">드롭다운</option>
</select>
</div>
</div>
<div className={optionContainer}>
{
{item.type !== 'textarea' && (
<>
{item.options.map((option, i) => (
{item.options?.map((option, i) => (
<div key={i} className={optionWrapper}>
{item.type === 'checkbox' && <Checkbox disabled />}
{item.type === 'radio' && <Radio disabled />}
Expand All @@ -118,7 +118,12 @@ const SurveyItem = ({
옵션추가
</button>
</>
}
)}
{item.type === 'textarea' && (
<div>
<input placeholder="단답형 텍스트" disabled />
</div>
)}
</div>
<div className={itemFooterContainer}>
<div className={itemFooterWrapper}>
Expand Down
9 changes: 4 additions & 5 deletions apps/survey-admin/src/app/hooks/query/useServey.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useSupabaseContext } from '@ssoon-servey/supabase';
import { z } from 'zod';
import { itemsType } from '../../types/items.type';

const OptionSchema = z.object({
text: z.string().min(1, { message: '텍스트를 입력해주세요' }),
});

const SurveyItemSchema = z.object({
title: z.string().min(1, { message: '항목의 제목을 입력해주세요' }),
type: z.enum(['radio', 'select', 'checkbox']),
type: z.enum(['radio', 'select', 'checkbox', 'textarea']),
required: z.boolean(),
options: z.array(OptionSchema),
options: z.array(OptionSchema).nullable(),
});

const SurveySectionSchema = z.object({
Expand Down Expand Up @@ -62,9 +63,8 @@ export const useCreateSurvey = () => {
const insertItems: {
section_id: number;
question_title: string;
question_type: 'radio' | 'select' | 'checkbox' | 'textarea';
question_type: itemsType;
question_required: boolean;
hasOption: boolean;
}[] = [];

sectionIds.forEach((id, i) => {
Expand All @@ -74,7 +74,6 @@ export const useCreateSurvey = () => {
question_title: item.title,
question_type: item.type,
question_required: item.required,
hasOption: !!item.options,
})
);
});
Expand Down
33 changes: 18 additions & 15 deletions apps/survey-admin/src/app/hooks/viewmodel/useSurveyViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
type SurveySection,
type Survey,
Expand All @@ -8,6 +8,7 @@ import {
import { insertItem } from '@ssoon-servey/utils';
import { useState } from 'react';
import { produce } from 'immer';
import { itemsType } from '../../types/items.type';

let id = 0;
const genId = () => {
Expand All @@ -27,16 +28,11 @@ const newSection = (): SurveySection & { id: number } => ({
});

const useSurveyViewModel = () => {
const [toolbarTop, setToolbarTop] = useState(0);
const [survey, setSurvey] = useState<Omit<Survey, 'sections'>>({
title: '제목 없는 설문지',
description: '',
});

// 0 0
// 0 1

// 1 0으로 되어야 하는데 현재 1 1
const [currentActiveItemIndex, setCurrentActiveItemIndex] = useState({
sectionId: 0,
itemId: 0,
Expand Down Expand Up @@ -79,9 +75,11 @@ const useSurveyViewModel = () => {
produce(sections, (sections) => {
const section = sections[sectionId];
const item = section.items[itemId];
item.options.push({
text: `옵션 ${item.options.length + 1}`,
});
if (item.options) {
item.options.push({
text: `옵션 ${item.options.length + 1}`,
});
}
})
);
};
Expand All @@ -98,8 +96,7 @@ const useSurveyViewModel = () => {
});
};

const handleActiveItem = (sectionId: number, itemId: number, top: number) => {
setToolbarTop(top);
const handleActiveItem = (sectionId: number, itemId: number) => {
setCurrentActiveItemIndex({ sectionId, itemId });
};

Expand Down Expand Up @@ -127,14 +124,17 @@ const useSurveyViewModel = () => {
);
};

const handleChangeItemType = (type: 'checkbox' | 'radio' | 'select') => {
const handleChangeItemType = (type: itemsType) => {
const { sectionId, itemId } = currentActiveItemIndex;

setSurveySections((sections) =>
produce(sections, (sections) => {
const section = sections[sectionId];
const item = section.items[itemId];
item.type = type;
if (item.type === 'textarea') {
item.options = null;
}
})
);
};
Expand All @@ -146,8 +146,10 @@ const useSurveyViewModel = () => {
produce(sections, (sections) => {
const section = sections[sectionId];
const item = section.items[itemId];
const option = item.options[optionIndex];
option.text = value;
if (item.options) {
const option = item.options[optionIndex];
option.text = value;
}
})
);
};
Expand All @@ -172,7 +174,9 @@ const useSurveyViewModel = () => {
items: section.items,
})),
});
window.location.reload();
};

return {
survey,
handleSurveyInput,
Expand All @@ -188,7 +192,6 @@ const useSurveyViewModel = () => {
handleAddSections,
onSubmit,
currentActiveItemIndex,
toolbarTop,
};
};

Expand Down
1 change: 1 addition & 0 deletions apps/survey-admin/src/app/types/items.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type itemsType = 'radio' | 'select' | 'checkbox' | 'textarea';