한컴AI 2기

한컴AI 2기[스나이퍼팩토리] 한컴AI 2기 - AI개발자 교육 5주차

싱커 2025. 7. 30. 17:59

깃 관리

앞으로는 PR용도의 개인 별 브랜치를 병합 후 삭제하는 방식으로 가기로 했다. Merge한 후 Delete 브랜치하여 Remote브랜치를 없앤 다음, 다시 main브랜치에서 새 브랜치를 체크아웃하기로 했다.


설문 응답 페이지 (실제 설문 제출) 구현

응답 폼 예제

/take/:id 라우트에 설문 응답 폼을 구현했다.

TODO : 필수 응답 미응답시 막는 로직 필요.


응답이 있는 설문 삭제

FK 제약조건때문에, 응답이 있는 설문은 함부로 지울 수 없다. ON DELETE CASCADE로 설정을 바꿔줘야했다.

-- 1. 기존 외래키 제약조건 제거
ALTER TABLE answers DROP CONSTRAINT answers_question_id_fkey;

-- 2. ON DELETE CASCADE로 다시 생성
ALTER TABLE answers
ADD CONSTRAINT answers_question_id_fkey
FOREIGN KEY (question_id)
REFERENCES questions(id)
ON DELETE CASCADE;

confdeltype

해당 쿼리가 적용된 후의 모습이다. 이제 폼이 지워지면 q.id와 r.id가 모두 사라진다.


질문 유형 확장

다른 유형의 질문을 대응하기 위해, question  테이블에 컬럼을 추가해야했다.

ALTER TABLE public.questions
ADD COLUMN unit numeric(3,1),               -- 별점 단위 (예: 0.5, 1.0)
ADD COLUMN min integer,                    -- 점수형 최소값
ADD COLUMN max integer,                    -- 점수형 최대값
ADD COLUMN leftLabel varchar(100),         -- 점수형 좌측 라벨
ADD COLUMN rightLabel varchar(100);        -- 점수형 우측 라벨

그리고 트랜잭션 함수도 확장했다.

더보기
create or replace function save_form_with_questions(payload jsonb)
returns void
language plpgsql
as $$
declare
  p_form_id uuid;
  p_title text;
  p_description text;
  p_start_time timestamp;
  p_end_time timestamp;
  p_questions jsonb;
  q jsonb;
  o jsonb;
  q_type text;
begin
  -- 1. 폼 메타데이터 파싱
  p_form_id := (payload->>'form_id')::uuid;
  p_title := payload->>'title';
  p_description := payload->>'description';
  p_questions := payload->'questions';
  p_start_time := (payload->>'start_time')::timestamp;
  p_end_time := (payload->>'end_time')::timestamp;

  -- 2. 폼 정보 갱신
  update forms
  set title = p_title,
      description = p_description,
      start_time = p_start_time,
      end_time = p_end_time,
      updated_at = now()
  where id = p_form_id;

  -- 3. 기존 질문 삭제 (CASCADE로 answers/options 자동 정리)
  delete from questions where form_id = p_form_id;

  -- 4. 새로운 질문들 삽입
  for q in select * from jsonb_array_elements(p_questions)
  loop
    q_type := q->>'type';

    insert into questions (
      id,
      form_id,
      text,
      type,
      order_number,
      required,
      unit,
      min,
      max,
      left_label,
      right_label
    )
    values (
      (q->>'id')::uuid,
      p_form_id,
      q->>'text',
      q_type,
      (q->>'order_number')::int,
      (q->>'required')::boolean,
      (q->>'unit')::numeric,
      (q->>'min')::int,
      (q->>'max')::int,
      q->>'left_label',
      q->>'right_label'
    );

    -- 5. 객관식/드롭다운/체크박스만 옵션 삽입
    if q_type in ('radio', 'dropdown', 'checkbox') and q ? 'options' then
      for o in select * from jsonb_array_elements(q->'options')
      loop
        insert into options (id, question_id, label, value, order_number)
        values (
          (o->>'id')::uuid,
          (q->>'id')::uuid,
          o->>'label',
          o->>'value',
          (o->>'order_number')::int
        );
      end loop;
    end if;

  end loop;
end;
$$;

옵션이 있는 질문과 옵션이 없는 질문을 각각 별도로 처리한다.


설문 결과 통계

설문결과 통계 UI

별점형에 대하여 도수분포와 평균 GUI를 구현했다.

1일차 종료.


잔여 개발 리스트업

🧩 1. 응답 기능 고도화
[ ] 필수 응답 미입력 시 제출 방지 (required)
[ ] 객관식 기타(직접입력) 응답 처리
[ ] 복수선택 질문 유형 (checkbox) 추가 -
[ ] 설문당 중복 응답 방지 로직

---

📊 2. 분석/시각화 기능 확장
[ ] 점수형(score) 질문 → 평균 + 분포 바차트 시각화
[ ] 주관식 응답 → 키워드 추출 / 단어 클라우드 / 텍스트 정렬
[ ] 설문별 응답 통계 요약 (총 응답 수, 완료율 등)

---

🎨 3. UI/UX 개선 및 구조화
[ ] StarDisplay, ScoreDisplay 등 공용화
[ ] 반응형 대응 (모바일 뷰 UX 개선)
[ ] 로딩/에러 상태 처리 (fetch 중 스피너, supabase 에러 핸들링)
[ ] 미응답 로직고도화...?
---

📤 4. 공유 및 배포 기능
[ ] 공유 링크 현재 response로 응답결과 공유중 || 결과공유(/response/:formId) / 설문참여 (/take/:formId)
[ ] 결과 공개 여부 설정 (results_public)
[ ] 결과 export 기능 (CSV, PDF 다운로드 or 프린트)

---

🔐 5. 테스트 및 보안
[ ] 단위 테스트 작성 (Question, StarRating 등)
[ ] Supabase RLS 설정 (user_id 기반 접근 제한)
[ ] 설문 종료일 이후 응답 차단 (form.end_time 기준)

개발자 3인이 모두 모였고, 남은 할 일들을 리스트업해봤다. 이 중 필수기능은 아닌 것들은 (adv)로 별도로 분리했다.

 

 


프로젝트 관리

프로젝트 칸반보드

칸반보드에 이슈들을 리스트업했다. 그리고 자신이 한||할 일들은 Assignees에 self 할당을 했다.

이전까지 해온 협업들 중 가장 수준이 높아진 것 같다.


점수형 컴포넌트

컴포넌트에 데이터 담기 및 질문 좌우라벨 보이기등의 작업을 했다.

모든 내용이 자기 자리에 맞게 보인다. 좀 더 예쁘게 만들 고민은 필요하다.


드롭다운 유형 UI 구현

드롭다운은 객관식과 동질적이다.

평범한 UI로 만들었고, 잘 작동한다.


응답이 있는 폼 질문 추가 방지

ON DELETE CASCADE 설정으로 무결성이 지켜진다고 생각했는데, 응답이 있는 설문에 질문을 추가할 경우 큰 문제가 생겼다. 폼에 질문이 추가될 때, 이를 submit하면 기존의 question들을 드롭하고 새로 질문을 삽입한다. (rpc함수 트랜잭션이 그렇게 기능한다.) 즉, 질문을 추가하면 해당 폼의 모든 응답 내용들이 drop되는것이다...

별도의 로직을 추가하여 해소했다.


리팩토링

네이버의 설문 종료 시스템에 대한 이해를 가지고, isOngoing함수를 전역 유틸로 관리하기로 했다.

export const isOngoing = (endTime: Date | undefined): boolean => {
  if (!endTime) return true;
  return endTime > new Date();
};

endTime이 현재 시각을 초과하였다면 false가 반환되는 심플한 코드이다.

향후 설문 강제종료 기능에 사용할 것이다.

2일차 종료.

 

 


설문 강제 종료

강제 종료 UI

설문 강제종료 기능을 구현했다.

 

확인창을 띄워주고, DB상에서는 종료시각을 강제종료 누른 순간으로 세팅해준다. 종료상태 표기는 isOngoing 유틸함수의 몫.


종료된 설문 응답 차단 UI

계속해서, 종료된 설문에 응답을 막는 기능 및 UI를 구현했다.


필수질문 응답 체크 로직

필수 질문 누락 UI

required컬럼이 있지만 활용하지 못했었다. 질문마다 순회하며 체크하고, 채워지지 않았을 시 질문 제목을 포함하여 메세지를 띄운다.


다중 응답 처리 로직

응답한 설문은 네이버에서 분기 UI를 제공한다. 우리도 이와 동일한 방식을 사용하기로 했다.

이 부분 코드가 많이 복잡했지만, 결과만 보면 뿌듯하다.

답변 수정 시연 움짤

수정할 답변을 선택하는 UI가 존재하며, 제출 시 업데이트된다. (업데이트 로그는 추적하지 않는다.)


참여한 설문 UI 개선

 

네이버처럼 세로로 쌓는 flex UI로 개선했다. 폼id 기반의 바인딩은 별도 처리가 필요하다.


배포

쉬운 배포방식 3가지를 두고 고민했다. 넷플리파이, 파이어베이스, 깃허브페이지가 그것이다.

더보기

🚀 추천 순위

1. Netlify (추천 ✅)

  • 장점:
    • Vite + React SPA에 최적화
    • 환경변수 설정 UI 편리함 (SUPABASE_URL, SUPABASE_KEY 등)
    • 동적 라우팅을 위한 SPA fallback (_redirects 자동 처리 또는 netlify.toml 설정 가능)
    • GitHub 연동 매우 쉬움 (CI/CD 기본 제공)
    • 커스텀 도메인 및 프리뷰 브랜치 배포도 가능

2. Firebase Hosting

  • 장점:
    • 안정적이며 글로벌 CDN 제공
    • 커스텀 도메인 쉽게 연동 가능
  • 단점:
    • GitHub 자동 연동/배포는 Netlify만큼 간단하지 않음
    • 초기 셋업에 firebase.json 등 설정 파일 필요
    • Supabase와 함께 쓰는 경우에는 굳이 Firebase까지 쓸 필요 없음

3. GitHub Pages

  • 비추천 ❌
    • 동적 라우팅 지원 불가 (/take/:formId 같은 URL에서 404 발생 가능성 있음)
    • HashRouter로 우회 가능하나 react-router-dom의 BrowserRouter를 쓰고 있어 부적합
    • 환경변수 사용 어려움 (정적 자원만 서빙)

 

GPT의 추천에 따라 넷플리파이를 선택했고, 배포했다.

https://majestic-mermaid-d34d25.netlify.app/

4일차 종료.