개인정보-컴플라이언스-웹애플리케이션 (수정사항 4)

2025. 1. 29. 18:54프로젝트

728x90

시스템의 핵심 목표 이해하기

1️⃣ 개인정보 보호 수준 평가 편람(개인정보보호위원회) 기반의 자가진단 제공

✔️ 고정된 43개 정량 문항 + 8개 정성 문항을 기반으로 평가 진행합니다.

✔️ **기관회원(사용자)**은 공식 편람에 명시된 지표, 근거법령, 평가기준, 참고사항, 증빙자료, 증빙자료 예시 등의 정보를 참고하여 평가를 진행합니다.

✔️ 사용자가 직접 평가 지표를 수정하는 것이 아니라, 사전 정의된 문항을 기준으로 자가진단을 진행합니다.

2️⃣ 기관회원이 자가진단 수행 및 평가 결과 입력 가능

✔️ 각 문항별로 "이행", "미이행", "자문 필요" 등의 평가를 선택합니다.

✔️ 필요 시 추가 의견 입력 및 증빙 파일을 첨부 가능합니다.

✔️ 정량 평가 문항별로 사전에 설정된 배점을 반영하여 총점 및 등급을 자동 산출합니다.

✔️ 정성 평가 문항은 배점이 5점으로 고정되며, 기관회원의 응답 데이터를 저장합니다.

3️⃣ 전문가 회원이 기관회원의 평가 결과에 대한 피드백 제공

✔️ 전문가 회원은 기관회원이 제출한 자가진단 결과를 검토하고, 각 문항별 피드백을 입력하여 저장할 수 있습니다.

✔️ 기관회원이 "자문 필요"로 체크한 문항에 대해 전문가의 의견을 받을 수 있도록 지원합니다.

✔️ 전문가의 피드백을 반영하여 기관회원이 평가를 개선할 수 있도록 유도합니다.

4️⃣ 슈퍼유저(최고관리자)가 평가 문항 및 평가 기준 관리

✔️ 슈퍼유저는 시스템 내 평가 문항을 관리하며, 매년 변경될 수 있는 평가 기준을 수정할 수 있는 기능 제공

✔️ 정량 평가 문항(43개)과 정성 평가 문항(8개)에 대해 다음 항목을 수정 가능합니다.

  • 정량 평가: 지표, 근거법령, 평가기준, 참고사항, 증빙자료, 증빙자료 예시
  • 정성 평가: 지표, 지표 정의, 평가 기준, 참고사항✔️ 슈퍼유저가 변경한 평가 문항 및 기준은 즉시 반영되어 이후 자가진단을 수행하는 기관회원이 최신 기준을 적용하여 평가 가능합니다.

5️⃣ 자가진단 후 피드백을 반영하여 재진단 가능

✔️ 기관회원은 전문가의 피드백을 확인한 후, 필요 시 추가 조치를 반영하여 자가진단을 다시 수행 가능합니다

✔️ 반복적인 개선 과정을 통해 기관의 개인정보 보호 수준을 점진적으로 향상합니다

 

결론요약

슈퍼유저에서 매년마다 바뀌는 기준에따라 정성문항,정량문항 지침 글을 수정해주기위해선 기존의 db구조 바껴야함

기존 코드에서는 다음과 같은 문제점이 존재함.

현재 시스템에서는 기관회원이 시스템을 등록하고 자가진단을 수행할 때마다 기존 문항을 조회하여 응답을 입력하는 것이 아니라, 정량·정성 문항 자체를 새로운 데이터로 추가하면서 응답까지 함께 저장하는 방식으로 동작하고 있습니다.

이로 인해, 동일한 문항(question_number=1 등)이 시스템별로 중복 삽입되며, 기관회원이 자가진단을 수행할 때마다 불필요한 데이터가 계속해서 추가되는 문제가 발생합니다

  • 문항 테이블(quantitative, qualitative)에서 동일한 문항이 시스템별로 중복 생성합니다.
  • 응답 데이터와 함께 문항 데이터까지 합쳐서 계속 삽입됩니다.0
  • 데이터 저장 공간을 불필요하게 차지하고, 성능 저하 및 관리의 어려움을 초래합니다.

 

DB + 코드 분석

CREATE TABLE quantitative (
    id INT AUTO_INCREMENT PRIMARY KEY,
    question_number INT NOT NULL,
    unit VARCHAR(50) DEFAULT NULL,
    evaluation_method ENUM('정량평가', '배점') DEFAULT '정량평가',
    score DECIMAL(5,2) DEFAULT NULL,
    question TEXT,
    legal_basis TEXT DEFAULT NULL,
    criteria_and_references TEXT DEFAULT NULL,
    file_upload VARCHAR(255) DEFAULT NULL,
    response ENUM('이행', '미이행', '해당없음', '자문 필요') DEFAULT NULL,
    additional_comment TEXT DEFAULT NULL,
    feedback TEXT DEFAULT NULL,
    system_id INT NOT NULL,
    FOREIGN KEY (system_id) REFERENCES systems(id) ON DELETE CASCADE
);

CREATE TABLE qualitative (
    id INT AUTO_INCREMENT PRIMARY KEY,
    question_number INT NOT NULL COMMENT '지표 번호',
    indicator TEXT NOT NULL COMMENT '지표',
    indicator_definition TEXT DEFAULT NULL COMMENT '지표 정의',
    evaluation_criteria TEXT DEFAULT NULL COMMENT '평가기준 (착안사항)',
    reference_info TEXT DEFAULT NULL COMMENT '참고사항',
    file_path VARCHAR(255) DEFAULT NULL COMMENT '파일 업로드 경로',
    response ENUM('자문필요', '해당없음') DEFAULT NULL COMMENT '응답 상태',
    additional_comment TEXT DEFAULT NULL COMMENT '추가 의견',
    feedback TEXT COMMENT '피드백',
    system_id INT NOT NULL COMMENT '시스템 ID',
    user_id INT NOT NULL COMMENT '사용자 ID',
    FOREIGN KEY (system_id) REFERENCES systems(id) ON DELETE CASCADE
);

 

🛑 quantitative(정량) 테이블 문제점 문항 데이터와 응답 데이터가 하나의 테이블에 같이 저장됩니다. → 기관회원이 시스템을 등록할 때마다 동일한 문항이 quantitative 테이블에 중복 저장됨 점수 계산 시 불필요한 중복 조회합니다. → 시스템별로 같은 문항이 반복적으로 존재하여 불필요한 데이터 검색 연산이 발생 슈퍼유저가 문항을 수정할 수 없습니다. → 모든 시스템의 동일한 문항을 일괄 수정하기 어렵고, 변경 시마다 데이터를 재구성해야 합니다.

 

🛑 qualitative(정성) 테이블 문제점 정량과 동일하게 문항 데이터와 응답 데이터가 하나의 테이블에 같이 저장됩니다. → 시스템이 추가될 때마다 같은 문항이 중복 저장됨 응답과 피드백을 조회할 때 불필요한 중복 데이터 로드됩니다. → 기관회원이 새로운 시스템을 추가할 때마다 문항이 추가되는 방식이므로 데이터가 계속 쌓임 슈퍼유저가 문항을 수정하면 기존 데이터에 영향이 끼칩니다. → 슈퍼유저가 문항을 수정하려면 이미 저장된 데이터를 일괄 변경해야 하는 문제가 발생합니다.

 

✅ [해결책] 테이블 분리

자가진단 문항(정량,정성) 테이블 + 자가진단 응답(정량,정성) 테이블

✔️ 해결 방법

  1. 문항 데이터를 따로 저장하는 quantitative_questions , qualitative_questions 테이블을 생성
  2. 사용자 응답을 quantitative_responses , qualitative_responses 테이블에서 저장
  3. 해당 테이블을 question_id 기준으로 조인하여 데이터 조회
  4. 슈퍼유저가 문항을 수정할 수 있도록 API 추가

 

새로운 테이블 구조

✅ 1) 정량 문항 테이블 (quantitative_questions)

CREATE TABLE quantitative_questions (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 문항 ID
    question_number INT NOT NULL UNIQUE,  -- 문항 번호
    unit VARCHAR(50) DEFAULT NULL,  
    evaluation_method ENUM('정량평가', '배점') DEFAULT '정량평가',
    score DECIMAL(5,2) DEFAULT NULL,
    question TEXT,
    legal_basis TEXT DEFAULT NULL,
    criteria_and_references TEXT DEFAULT NULL
);

✅ 2) 정량 응답 테이블 (quantitative_responses)

CREATE TABLE quantitative_responses (
    id INT AUTO_INCREMENT PRIMARY KEY,
    system_id INT NOT NULL,
    user_id INT NOT NULL,
    question_id INT NOT NULL,  -- 문항 테이블과 연결
    response ENUM('이행', '미이행', '해당없음', '자문 필요') DEFAULT NULL,
    additional_comment TEXT DEFAULT NULL,
    feedback TEXT DEFAULT NULL,
    FOREIGN KEY (system_id) REFERENCES systems(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
    FOREIGN KEY (question_id) REFERENCES quantitative_questions(id) ON DELETE CASCADE
);

✅ 3) 정성 문항 테이블(qualitative_questions) 생성

CREATE TABLE qualitative_questions (
    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '문항 ID',
    question_number INT NOT NULL UNIQUE COMMENT '지표 번호',
    indicator TEXT NOT NULL COMMENT '지표',
    indicator_definition TEXT DEFAULT NULL COMMENT '지표 정의',
    evaluation_criteria TEXT DEFAULT NULL COMMENT '평가기준 (착안사항)',
    reference_info TEXT DEFAULT NULL COMMENT '참고사항'
);

✅ 4) 정성 응답 테이블(qualitative_responses) 생성

CREATE TABLE qualitative_responses (
    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '응답 ID',
    system_id INT NOT NULL COMMENT '시스템 ID',
    user_id INT NOT NULL COMMENT '사용자 ID',
    question_id INT NOT NULL COMMENT '문항 ID',
    response ENUM('자문필요', '해당없음') DEFAULT NULL COMMENT '응답 상태',
    additional_comment TEXT DEFAULT NULL COMMENT '추가 의견',
    feedback TEXT COMMENT '피드백',
    file_path VARCHAR(255) DEFAULT NULL COMMENT '파일 업로드 경로',
    FOREIGN KEY (system_id) REFERENCES systems(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE,
    FOREIGN KEY (question_id) REFERENCES qualitative_questions(id) ON DELETE CASCADE
);

 

3️⃣ 백엔드 API 수정 (정량 & 정성 분리)

 기존 API의 문제점

1. `handleQuantitativeSave`, `handleQualitativeSave`가 여전히 기존 `quantitative`, `qualitative` 테이블을 사용하고 있음.


2. 응답 저장 API (`handleQuantitativeSave`, `handleQualitativeSave`)
    - 응답을 저장할 때 `quantitative_responses`, `qualitative_responses` 테이블을 사용해야 하는데 여전히

`quantitative`, `qualitative` 테이블에 저장 중.


3. 문항 조회 API (`getQuantitativeData`, `getQualitativeData`)
    - 문항 데이터를 가져올 때 기존 `quantitative`, `qualitative` 테이블을 참조하고 있습니다.
    - 하지만 문항 정보는 `quantitative_questions`, `qualitative_questions` 테이블에서 가져와야 합니다.


4. 응답 데이터 조회
    - 기존 `getQuantitativeData`, `getQualitativeData` API는 문항과 응답 데이터를 함께 가져옵니다.
    - 하지만 응답은 `quantitative_responses`, `qualitative_responses` 테이블에서 가져와야 합니다.

 

📌 [수정전 코드]

// 정량 데이터 가져오기
const getQuantitativeData = async (req, res) => {
  const { systemId } = req.query;

  if (!systemId) {
    return res.status(400).json({ message: "System ID is required." });
  }

  try {
    const query = `
      SELECT question_number, unit, evaluation_method, score, question,
             legal_basis, criteria_and_references, file_upload, response,
             additional_comment, feedback
      FROM quantitative
      WHERE system_id = ?
    `;
    const [results] = await pool.query(query, [systemId]);
    res.status(200).json(results);
  } catch (error) {
    console.error("Error fetching quantitative data:", error.message);
    res
      .status(500)
      .json({ message: "Internal server error.", error: error.message });
  }
};

// 정성 데이터 가져오기
const getQualitativeData = async (req, res) => {
  const { systemId } = req.query;

  if (!systemId) {
    return res.status(400).json({ message: "System ID is required." });
  }

  try {
    const query = `
      SELECT question_number, indicator, indicator_definition, evaluation_criteria,
             reference_info, response, additional_comment, file_path
      FROM qualitative
      WHERE system_id = ?
    `;
    const [results] = await pool.query(query, [systemId]);
    res.status(200).json(results);
  } catch (error) {
    console.error("Error fetching qualitative data:", error.message);
    res
      .status(500)
      .json({ message: "Internal server error.", error: error.message });
  }
};

 

📌 [수정코드]

// 정량 데이터 조회
const getQuantitativeQuestions = async (req, res) => {
  try {
    const query = `SELECT * FROM quantitative_questions`;
    const [results] = await pool.query(query);
    res.status(200).json(results);
  } catch (error) {
    console.error("정량 문항 조회 실패:", error);
    res.status(500).json({ message: "서버 오류 발생" });
  }
};

// 정성 데이터 조회
const getQualitativeQuestions = async (req, res) => {
  try {
    const query = `SELECT * FROM qualitative_questions`;
    const [results] = await pool.query(query);
    res.status(200).json(results);
  } catch (error) {
    console.error("정성 문항 조회 실패:", error);
    res.status(500).json({ message: "서버 오류 발생" });
  }
};


// app.js 라우트 컨트롤러함수 변경 
app.get("/selftest/quantitative", requireAuth, getQuantitativeQuestions);
app.get("/selftest/qualitative", requireAuth, getQualitativeQuestions);

 

2) 응답 저장 API (수정)

📌 [수정전 코드]

const handleQuantitativeSave = async (req, res) => {
  const { quantitativeResponses } = req.body;

  if (!quantitativeResponses || !Array.isArray(quantitativeResponses)) {
    return res.status(400).json({ message: "Invalid quantitative responses format." });
  }

  try {
    const query = `
      INSERT INTO quantitative (
        question_number, unit, evaluation_method, score, question,
        legal_basis, criteria_and_references, file_upload, response,
        additional_comment, feedback, system_id
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE
        unit = VALUES(unit),
        evaluation_method = VALUES(evaluation_method),
        score = VALUES(score),
        legal_basis = VALUES(legal_basis),
        criteria_and_references = VALUES(criteria_and_references),
        file_upload = VALUES(file_upload),
        response = VALUES(response),
        additional_comment = VALUES(additional_comment),
        feedback = VALUES(feedback)
    `;

    for (const {
      questionNumber,
      unit,
      evaluationMethod,
      score,
      question,
      legalBasis,
      criteriaAndReferences,
      fileUpload,
      response: answer,
      additionalComment,
      feedback,
      systemId,
    } of quantitativeResponses) {
      await pool.query(query, [
        questionNumber,
        unit || "단위 없음",
        evaluationMethod || "정량평가",
        score || 0,
        question || "질문 없음",
        legalBasis || "근거 법령 없음",
        criteriaAndReferences || "평가기준 없음",
        fileUpload || null,
        answer || "응답 없음",
        additionalComment || "추가 의견 없음",
        feedback || "피드백 없음",
        systemId,
      ]);
    }

    res.status(200).json({ message: "Quantitative responses saved successfully." });
  } catch (error) {
    console.error("Error saving quantitative responses:", error.message);
    res.status(500).json({ message: "Internal server error.", error: error.message });
  }
};
const handleQualitativeSave = async (req, res) => {
  const {
    questionNumber,
    response,
    additionalComment,
    systemId,
    userId,
    indicator,
    indicatorDefinition,
    evaluationCriteria,
    referenceInfo,
    filePath,
  } = req.body;

  if (!questionNumber || !response || !systemId || !userId) {
    return res.status(400).json({ message: "필수 필드가 누락되었습니다." });
  }

  try {
    const query = `
      INSERT INTO qualitative (
        question_number, response, additional_comment, system_id, user_id, 
        indicator, indicator_definition, evaluation_criteria, reference_info, file_path
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE
        response = VALUES(response),
        additional_comment = VALUES(additional_comment),
        indicator = VALUES(indicator),
        indicator_definition = VALUES(indicator_definition),
        evaluation_criteria = VALUES(evaluation_criteria),
        reference_info = VALUES(reference_info),
        file_path = VALUES(file_path)
    `;

    await pool.query(query, [
      questionNumber,
      response,
      additionalComment || "",
      systemId,
      userId,
      indicator || "",
      indicatorDefinition || "",
      evaluationCriteria || "",
      referenceInfo || "",
      filePath || null,
    ]);
    res.status(200).json({ message: "Response saved successfully." });
  } catch (error) {
    console.error("Error saving qualitative response:", error.message);
    res.status(500).json({ message: "Internal server error.", error: error.message });
  }
};

 

 

🚨 기존 코드의 문제점

문제점 1️⃣ - 기존 quantitative 테이블을 사용 (테이블 구조 변경 반영 안 됨)

 

quantitative 테이블을 사용하고 있으나, 현재 quantitative_questions 와 quantitative_responses 로 분리됩니다.

이제 quantitative_questions는 문항만 저장, quantitative_responses는 사용자의 응답을 저장하는 테이블이므로 테이블이 변경된 구조를 반영해야 합니다.

문제점 2️⃣ - 사용자별 응답 데이터 저장 안됨

기존 코드에서는 user_id를 저장하는 로직이 없습니다.

따라서 같은 system_id에 대해 여러 명이 응답하면 누가 응답했는지 확인할 방법이 없습니다.

🚀 해결책: user_id를 추가하여 각 사용자의 응답을 저장하도록 수정합니다.

문제점 3️⃣ - 불필요한 데이터 저장

기존 코드에서는 문항 정보(question, legal_basis, evaluation_criteria 등)를 응답 저장 시 함께 저장합니다.

하지만 문항 정보는 quantitative_questions에 이미 저장되어 있음.🚀 해결책: quantitative_responses 테이블에는 응답(response, additional_comment, file_path)만 저장하도록 수정합니다.

 

📌 [수정 코드]

routes/selftest.js

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path),
        feedback = VALUES(feedback);
    `;

    for (const { systemId, questionId, response, additionalComment, filePath, feedback } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
        feedback || "피드백 없음",
      ]);
    }

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};
const saveQualitativeResponses = async (req, res) => {
  const { responses } = req.body;
  const user_id = req.session.user?.id;

  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path),
        feedback = VALUES(feedback);
    `;

    for (const { systemId, questionId, response, additionalComment, filePath, feedback } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
        feedback || "피드백 없음",
      ]);
    }

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

app.js

// 자기 평가 라우트
app.post("/selftest/quantitative", requireAuth, saveQuantitativeResponses);
app.post("/selftest/qualitative", requireAuth, saveQualitativeResponses);

 

 

3 ) 응답 데이터 조회 api 추가)

1) 정량 응답 조회 (getQuantitativeResponses)

const getQuantitativeResponses = async (req, res) => {
  const { systemId, userId } = req.query;

  if (!systemId || !userId) {
    return res.status(400).json({ message: "System ID and User ID are required." });
  }

  try {
    const query = `
      SELECT qq.question_number, qq.question, qq.evaluation_criteria, qq.legal_basis, qq.score,
             qr.response, qr.additional_comment, qr.file_path, qr.feedback
      FROM quantitative_responses qr
      JOIN quantitative_questions qq ON qr.question_id = qq.id
      WHERE qr.system_id = ? AND qr.user_id = ?;
    `;
    const [results] = await pool.query(query, [systemId, userId]);
    res.status(200).json(results);
  } catch (error) {
    console.error("Error fetching quantitative responses:", error.message);
    res.status(500).json({ message: "Internal server error.", error: error.message });
  }
};

 

2) 정성 응답 조회 (getQualitativeResponses)

const getQualitativeResponses = async (req, res) => {
  const { systemId, userId } = req.query;

  if (!systemId || !userId) {
    return res.status(400).json({ message: "System ID and User ID are required." });
  }

  try {
    const query = `
      SELECT qq.question_number, qq.indicator, qq.indicator_definition, qq.evaluation_criteria, qq.reference_info,
             qr.response, qr.additional_comment, qr.file_path, qr.feedback
      FROM qualitative_responses qr
      JOIN qualitative_questions qq ON qr.question_id = qq.id
      WHERE qr.system_id = ? AND qr.user_id = ?;
    `;
    const [results] = await pool.query(query, [systemId, userId]);
    res.status(200).json(results);
  } catch (error) {
    console.error("Error fetching qualitative responses:", error.message);
    res.status(500).json({ message: "Internal server error.", error: error.message });
  }
};

 

app.js

// 자기 평가 라우트
app.get(
  "/selftest/quantitative/responses",
  requireAuth,
  getQuantitativeResponses
);
app.get(
  "/selftest/qualitative/responses",
  requireAuth,
  getQualitativeResponses
);

 

 

삽입 API 하나로 덮어쓰는 방식이 더 적절

결론적으로, 삽입 API (saveQuantitativeResponses) 하나로 덮어쓰는 방식이 더 적절합니다.

데이터의 무결성을 유지하면서도 API를 최소화할 수 있습니다.

프론트엔드에서 더 간단한 로직을 유지할 수 있습니다.

수정 이력이 필요하면 updated_at 필드를 추가하여 해결 가능합니다.

 

1) updated_at 필드 추가

ALTER TABLE quantitative_responses ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
ALTER TABLE qualitative_responses ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

 

2) 기존 API (saveQuantitativeResponses) 수정

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path),
        feedback = VALUES(feedback),
        updated_at = CURRENT_TIMESTAMP;
    `;

    for (const { systemId, questionId, response, additionalComment, filePath, feedback } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
        feedback || "피드백 없음",
      ]);
    }

    res.status(200).json({ message: "응답 저장 및 업데이트 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

기존 API (saveQualitativeResponses) 수정

const saveQualitativeResponses = async (req, res) => {
  const { responses } = req.body;
  const user_id = req.session.user?.id;

  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path),
        feedback = VALUES(feedback),
        updated_at = CURRENT_TIMESTAMP;
    `;

    for (const {
      systemId,
      questionId,
      response,
      additionalComment,
      filePath,
      feedback,
    } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
        feedback || "피드백 없음",
      ]);
    }

    res.status(200).json({ message: "응답 저장 및 업데이트 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

 

보완할점

만약 사용자가 정량 진단시 43개 문항 중 일부만 제출하면 어떻게 될까?

현재 코드에서는 일부만 제출해도 저장이 가능합니다.

자가진단을 완료하려면 43개의 모든 문항이 채워져 있어야 합니다.

응답 저장 시 43개가 다 입력되었는지 검증하는 로직이 필요합니다.

 

 📌 (수정 코드) 정량 응답 저장 API

const saveQuantitativeResponses = async (req, res) => {
    const { responses } = req.body;
    const user_id = req.session.user?.id;

    if (!user_id) {
        return res.status(401).json({ message: "로그인이 필요합니다." });
    }

    if (!responses || !Array.isArray(responses)) {
        return res.status(400).json({ message: "응답 형식이 잘못되었습니다." });
    }

    // 1️⃣ 43개 문항이 모두 응답되었는지 확인
    if (responses.length !== 43) {
        return res.status(400).json({
            message: `정량 문항 응답 개수가 부족합니다. 현재 ${responses.length}개 응답됨, 43개 문항에 대한 응답이 필요합니다.`,
        });
    }

    try {
        const query = `
            INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
            ON DUPLICATE KEY UPDATE 
                response = VALUES(response), 
                additional_comment = VALUES(additional_comment),
                file_path = VALUES(file_path),
                feedback = VALUES(feedback),
                updated_at = NOW();
        `;

        for (const {
            systemId,
            questionId,
            response,
            additionalComment,
            filePath,
            feedback,
        } of responses) {
            await pool.query(query, [
                systemId,
                user_id,
                questionId,
                response || "응답 없음",
                additionalComment || "추가 의견 없음",
                filePath || null,
                feedback || "피드백 없음",
            ]);
        }

        res.status(200).json({ message: "모든 응답이 정상적으로 저장되었습니다." });
    } catch (error) {
        console.error("응답 저장 실패:", error.message);
        res.status(500).json({ message: "서버 오류 발생", error: error.message });
    }
};

 

 

✅ 추가된 기능

🔹 1. 응답 개수 검증 (responses.length === 43)

  • 사용자가 43개의 응답을 입력하지 않으면 저장 불가능.
  • 예외 메시지: 정량 문항 응답 개수가 부족합니다. 현재 40개 응답됨, 43개 문항에 대한 응답이 필요합니다.

🔹 2. updated_at 추가

  • 사용자가 기존 응답을 수정하면 자동으로 수정 시간 업데이트됨.

 

 

 📌 (수정 코드) 정성 응답 저장 API도 같은 로직 추가

const saveQualitativeResponses = async (req, res) => {
    const { responses } = req.body;
    const user_id = req.session.user?.id;

    if (!user_id) {
        return res.status(401).json({ message: "로그인이 필요합니다." });
    }

    if (!responses || !Array.isArray(responses)) {
        return res.status(400).json({ message: "응답 형식이 잘못되었습니다." });
    }

    // 1️⃣ 8개 문항이 모두 응답되었는지 확인
    if (responses.length !== 8) {
        return res.status(400).json({
            message: `정성 문항 응답 개수가 부족합니다. 현재 ${responses.length}개 응답됨, 8개 문항에 대한 응답이 필요합니다.`,
        });
    }

    try {
        const query = `
            INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path, feedback, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
            ON DUPLICATE KEY UPDATE 
                response = VALUES(response), 
                additional_comment = VALUES(additional_comment),
                file_path = VALUES(file_path),
                feedback = VALUES(feedback),
                updated_at = NOW();
        `;

        for (const {
            systemId,
            questionId,
            response,
            additionalComment,
            filePath,
            feedback,
        } of responses) {
            await pool.query(query, [
                systemId,
                user_id,
                questionId,
                response || "응답 없음",
                additionalComment || "추가 의견 없음",
                filePath || null,
                feedback || "피드백 없음",
            ]);
        }

        res.status(200).json({ message: "모든 응답이 정상적으로 저장되었습니다." });
    } catch (error) {
        console.error("응답 저장 실패:", error.message);
        res.status(500).json({ message: "서버 오류 발생", error: error.message });
    }
};

 

보완할점 2

정량 , 정성 평가 초기 설문조사 시 피드백 필드 제외해야합니다.

현재 초기 자가진단 설문조사를 진행하는 과정에서 피드백(feedback) 필드가 포함되어 있는데,

피드백은 전문가가 남기는 정보이므로 초기 사용자(기관회원)가 응답할 때는 필요하지 않습니다.

 

✔ 기존 코드 (잘못된 방식 - feedback 포함됨)

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, filePath, feedback)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path),
        feedback = VALUES(feedback);
    `;

    for (const {
      systemId,
      questionId,
      response,
      additionalComment,
      filePath,
      feedback,  // 🚨 초기에는 불필요한 필드
    } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
        feedback || "피드백 없음", // ❌ 불필요한 초기값
      ]);
    }

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

✔ 수정된 코드 (올바른 방식 - feedback 필드 제거)

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path);
    `;

    for (const {
      systemId,
      questionId,
      response,
      additionalComment,
      filePath,
    } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
      ]);
    }

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

정성 응답 저장 api도 마찬가지로

const saveQualitativeResponses = async (req, res) => {
  const { responses } = req.body;
  const user_id = req.session.user?.id;

  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = VALUES(additional_comment),
        file_path = VALUES(file_path);
    `;

    for (const {
      systemId,
      questionId,
      response,
      additionalComment,
      filePath,
    } of responses) {
      await pool.query(query, [
        systemId,
        user_id,
        questionId,
        response || "응답 없음",
        additionalComment || "추가 의견 없음",
        filePath || null,
      ]);
    }

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

 

보완할점 3

사용자가 평가 - 자문필요를 선택해야 자문필요사항을 덧붙여서 서버에 요청을 보낼수있습니다.

 

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = CASE 
          WHEN VALUES(response) = '자문 필요' THEN VALUES(additional_comment) 
          ELSE NULL 
        END,
        file_path = VALUES(file_path);
    `;

    const connection = await pool.getConnection();
    await connection.beginTransaction();

    for (const { systemId, questionId, response, additionalComment, filePath } of responses) {
      // "자문 필요"가 아닐 경우 additionalComment를 null로 설정
      const safeAdditionalComment = response === "자문 필요" ? additionalComment || "자문 요청" : null;

      await connection.query(query, [
        systemId,
        user_id,
        questionId,
        response,
        safeAdditionalComment,
        filePath || null,
      ]);
    }

    await connection.commit();
    connection.release();

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

const saveQualitativeResponses = async (req, res) => {
  const { responses } = req.body;
  const user_id = req.session.user?.id;

  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  try {
    const query = `
      INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = CASE 
          WHEN VALUES(response) = '자문 필요' THEN VALUES(additional_comment) 
          ELSE NULL 
        END,
        file_path = VALUES(file_path);
    `;

    const connection = await pool.getConnection();
    await connection.beginTransaction();

    for (const { systemId, questionId, response, additionalComment, filePath } of responses) {
      // "자문 필요"가 아닐 경우 additionalComment를 null로 설정
      const safeAdditionalComment = response === "자문 필요" ? additionalComment || "자문 요청" : null;

      await connection.query(query, [
        systemId,
        user_id,
        questionId,
        response,
        safeAdditionalComment,
        filePath || null,
      ]);
    }

    await connection.commit();
    connection.release();

    res.status(200).json({ message: "정성 응답 저장 완료" });
  } catch (error) {
    console.error("정성 응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

✅ response 값이 "자문 필요" 일 때만 additionalComment 저장합니다

✅ response 값이 "자문 필요" 가 아닌 경우, additionalComment 는 null 처리합니다.

✅ ON DUPLICATE KEY UPDATE 를 이용해 기존 응답을 덮어쓰도록 유지합니다.

 

 

보완할점 4

quantitative_questions (정성 문항 테이블 관련 삽입된 데이터에오류가있어서 삭제하고 다시 데이터 추가함)

-- 1. 테이블 데이터 완전히 삭제 후 초기화

DELETE FROM quantitative_questions;
DELETE FROM qualitative_questions;
-- 2. AUTO_INCREMENT 값을 초기화

ALTER TABLE quantitative_questions AUTO_INCREMENT = 1;

ALTER TABLE qualitative_questions AUTO_INCREMENT = 1;

INSERT INTO quantitative_questions (question_number, question, evaluation_criteria, legal_basis, score)
VALUES 
(1, '개인정보 보호 정책이 수립되어 있는가?', '정책 문서화 여부', '개인정보 보호법 제29조', 5),
(2, '개인정보 보호 교육이 정기적으로 이루어지는가?', '교육 시행 여부', '개인정보 보호법 제30조', 5),
(3, '개인정보 보호책임자가 지정되어 있는가?', '책임자 지정 여부', '개인정보 보호법 제31조', 5),
(4, '개인정보 보호 대책이 명확하게 정의되어 있는가?', '보호 대책 명확성', '개인정보 보호법 제32조', 5),
(5, '비밀번호 정책이 시행되고 있는가?', '비밀번호 설정 및 변경 정책', '개인정보 보호법 제33조', 5),
(6, '개인정보 암호화가 적절히 수행되는가?', '암호화 적용 여부', '개인정보 보호법 제34조', 5),
(7, '개인정보 접근 통제 정책이 마련되어 있는가?', '접근 통제 여부', '개인정보 보호법 제35조', 5),
(8, '개인정보 보관 및 파기 정책이 수립되어 있는가?', '보관 기간 및 파기 기준', '개인정보 보호법 제36조', 5),
(9, '개인정보 이용 동의 절차가 적절히 운영되고 있는가?', '이용 동의 절차 여부', '개인정보 보호법 제37조', 5),
(10, '개인정보 처리방침이 공시되어 있는가?', '처리방침 공개 여부', '개인정보 보호법 제38조', 5),
(11, '개인정보 보호 관련 내부 점검이 정기적으로 이루어지는가?', '내부 점검 주기 및 결과 관리', '개인정보 보호법 제39조', 5),
(12, '개인정보 보호 대책이 최신 법령을 반영하고 있는가?', '법령 반영 여부', '개인정보 보호법 제40조', 5),
(13, '개인정보 보호를 위한 모니터링 시스템이 운영되고 있는가?', '모니터링 시스템 구축 여부', '개인정보 보호법 제41조', 5),
(14, '개인정보 보호를 위한 보안 장비가 도입되어 있는가?', '보안 장비 도입 여부', '개인정보 보호법 제42조', 5),
(15, '개인정보 처리 시스템에 대한 보안 점검이 이루어지는가?', '시스템 보안 점검 여부', '개인정보 보호법 제43조', 5),
(16, '개인정보 보호를 위한 위협 대응 체계가 마련되어 있는가?', '위협 대응 절차 여부', '개인정보 보호법 제44조', 5),
(17, '개인정보 보호를 위한 내부 감사를 수행하는가?', '내부 감사 실시 여부', '개인정보 보호법 제45조', 5),
(18, '개인정보 유출 사고 대응 계획이 마련되어 있는가?', '유출 사고 대응 여부', '개인정보 보호법 제46조', 5),
(19, '개인정보 보호책임자 교육이 정기적으로 이루어지는가?', '책임자 교육 여부', '개인정보 보호법 제47조', 5),
(20, '개인정보 처리자가 보안 서약을 하고 있는가?', '보안 서약 실시 여부', '개인정보 보호법 제48조', 5),
(21, '개인정보 처리 업무가 외부 위탁될 경우 계약이 적절히 이루어지는가?', '위탁 계약 체결 여부', '개인정보 보호법 제49조', 5),
(22, '외부 위탁 업체의 개인정보 보호 수준을 정기적으로 점검하는가?', '위탁 업체 점검 여부', '개인정보 보호법 제50조', 5),
(23, '개인정보 보호 대책이 국제 표준을 준수하고 있는가?', '국제 표준 준수 여부', '개인정보 보호법 제51조', 5),
(24, '개인정보 보호 조치가 비용 대비 효과적인가?', '비용 대비 효과 분석 여부', '개인정보 보호법 제52조', 5),
(25, '개인정보 보호 관련 법률 개정 사항을 반영하고 있는가?', '법률 개정 반영 여부', '개인정보 보호법 제53조', 5),
(26, '개인정보 보호 교육이 모든 직원에게 제공되고 있는가?', '교육 제공 여부', '개인정보 보호법 제54조', 5),
(27, '개인정보 보호 정책이 지속적으로 개선되고 있는가?', '지속적인 개선 여부', '개인정보 보호법 제55조', 5),
(28, '개인정보 보호 대책이 기술 발전을 반영하고 있는가?', '최신 기술 반영 여부', '개인정보 보호법 제56조', 5),
(29, '개인정보 보호 사고 사례가 공유되고 있는가?', '사고 사례 공유 여부', '개인정보 보호법 제57조', 5),
(30, '개인정보 보호 조치가 내부 규정에 따라 점검되고 있는가?', '내부 규정 준수 여부', '개인정보 보호법 제58조', 5),
(31, '개인정보 보호 계획이 적절히 이행되고 있는가?', '계획 이행 여부', '개인정보 보호법 제59조', 5),
(32, '개인정보 보호를 위한 보안 인증을 취득하였는가?', '보안 인증 여부', '개인정보 보호법 제60조', 5),
(33, '개인정보 보호 대책이 최신 법률 및 가이드라인을 따르고 있는가?', '법률 준수 여부', '개인정보 보호법 제61조', 5),
(34, '개인정보 보호 정책이 전체 직원에게 전달되고 있는가?', '정책 전달 여부', '개인정보 보호법 제62조', 5),
(35, '개인정보 보호를 위한 기술이 적절히 활용되고 있는가?', '보호 기술 활용 여부', '개인정보 보호법 제63조', 5),
(36, '개인정보 보호 대책이 기업 문화로 정착되고 있는가?', '기업 문화 정착 여부', '개인정보 보호법 제64조', 5),
(37, '개인정보 보호를 위한 보안 절차가 준수되고 있는가?', '보안 절차 준수 여부', '개인정보 보호법 제65조', 5),
(38, '개인정보 보호 계획이 경영진의 승인 하에 이루어지는가?', '경영진 승인 여부', '개인정보 보호법 제66조', 5),
(39, '개인정보 보호 조치가 데이터 보호 요구 사항을 충족하는가?', '데이터 보호 충족 여부', '개인정보 보호법 제67조', 5),
(40, '개인정보 보호 교육이 외부 전문가에 의해 제공되는가?', '외부 전문가 교육 여부', '개인정보 보호법 제68조', 5),
(41, '개인정보 보호 점검 결과가 보고되고 있는가?', '점검 보고 여부', '개인정보 보호법 제69조', 5),
(42, '개인정보 보호 위반 시 제재 조치가 마련되어 있는가?', '제재 조치 마련 여부', '개인정보 보호법 제70조', 5),
(43, '개인정보 보호 조치가 산업별 규제를 따르고 있는가?', '산업 규제 준수 여부', '개인정보 보호법 제71조', 5);

 

보완할 점 6

- ENUM(`이행`, `미이행`, `해당없음`, `자문 필요`) 값 검증이 없습니다.
    - 사용자가 잘못된 응답을 제출하면 DB에 삽입 시 `Data truncated for column 'response'` 오류 발생합니다.
    - 해결: 서버에서 ENUM 값 검증 추가합니다.
- "자문 필요" 응답일 경우 `additional_comment` 필드가 `NULL`로 저장될 가능성이 있습니다.
    - "자문 필요" 선택 시 추가 의견을 필수로 요구하지만, `NULL`이 허용되면 데이터 정합성이 깨집니다.
    - 해결: "자문 필요"일 경우 기본값 `"자문 요청"`을 추가합니다.
- 파일 첨부(`file_path`)가 누락될 가능성
    - 클라이언트에서 파일이 업로드되지 않았을 경우 `NULL`로 처리해야 합니다.
    - 해결: **`file_path`가 없을 경우 `NULL`로 저장하도록 명확히 설정합니다.

 

 

saveQuantitativeResponses

const saveQuantitativeResponses = async (req, res) => {
  const { responses } = req.body;

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  const user_id = req.session.user?.id;
  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  try {
    const query = `
      INSERT INTO quantitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = CASE 
          WHEN VALUES(response) = '자문 필요' THEN VALUES(additional_comment) 
          ELSE NULL 
        END,
        file_path = VALUES(file_path);
    `;

    const connection = await pool.getConnection();
    await connection.beginTransaction();

    for (const { systemId, questionId, response, additionalComment, filePath } of responses) {
      // ✅ response 값이 ENUM 내에 있는지 확인
      if (!["이행", "미이행", "해당없음", "자문 필요"].includes(response)) {
        throw new Error(`Invalid response value: ${response}`);
      }

      // ✅ "자문 필요"일 경우 additional_comment 기본값 설정
      const safeAdditionalComment =
        response === "자문 필요" ? additionalComment || "자문 요청" : null;

      await connection.query(query, [
        systemId,
        user_id,
        questionId,
        response,
        safeAdditionalComment,
        filePath || null, // ✅ 파일 첨부 필드 유지
      ]);
    }

    await connection.commit();
    connection.release();

    res.status(200).json({ message: "응답 저장 완료" });
  } catch (error) {
    console.error("응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

saveQualitativeResponses

const saveQualitativeResponses = async (req, res) => {
  const { responses } = req.body;
  const user_id = req.session.user?.id;

  if (!user_id) {
    return res.status(401).json({ message: "로그인이 필요합니다." });
  }

  if (!responses || !Array.isArray(responses)) {
    return res.status(400).json({ message: "Invalid responses format." });
  }

  try {
    const query = `
      INSERT INTO qualitative_responses (system_id, user_id, question_id, response, additional_comment, file_path)
      VALUES (?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE 
        response = VALUES(response), 
        additional_comment = CASE 
          WHEN VALUES(response) = '자문 필요' THEN VALUES(additional_comment) 
          ELSE NULL 
        END,
        file_path = VALUES(file_path);
    `;

    const connection = await pool.getConnection();
    await connection.beginTransaction();

    for (const { systemId, questionId, response, additionalComment, filePath } of responses) {
      // ✅ ENUM 값 검증
      if (!["자문 필요", "해당없음"].includes(response)) {
        throw new Error(`Invalid response value: ${response}`);
      }

      // ✅ "자문 필요"일 경우 `additional_comment` 기본값 설정
      const safeAdditionalComment =
        response === "자문 필요" ? additionalComment?.trim() || "자문 요청" : null;

      await connection.query(query, [
        systemId,
        user_id,
        questionId,
        response,
        safeAdditionalComment,
        filePath || null, // ✅ 파일 첨부 필드 유지
      ]);
    }

    await connection.commit();
    connection.release();

    res.status(200).json({ message: "정성 응답 저장 완료" });
  } catch (error) {
    console.error("정성 응답 저장 실패:", error.message);
    res.status(500).json({ message: "서버 오류 발생", error: error.message });
  }
};

 

프론트엔드에서 api 연결 및 기능 구현

🚨 수정된 코드들

 

DiagnosisPage.js

import React, { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import axios from "axios";
import { useRecoilState } from "recoil";
import {
  quantitativeDataState,
  responsesState,
  currentStepState,
} from "../../state/selfTestState";

function DiagnosisPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const { userId, systemId, isReevaluation } = location.state || {}; // ✅ 다시 평가 여부 추가

  const [quantitativeData, setQuantitativeData] = useRecoilState(
    quantitativeDataState
  );
  const [responses, setResponses] = useRecoilState(responsesState);
  const [currentStep, setCurrentStep] = useRecoilState(currentStepState);
  const [hasFeedback, setHasFeedback] = useState(false); // ✅ 피드백 존재 여부 상태 추가

  // 🔹 정량 설문 문항 불러오기
  useEffect(() => {
    if (!userId || !systemId) {
      console.error("❌ Missing userId or systemId:", { userId, systemId });
      alert("시스템 또는 사용자 정보가 누락되었습니다. 대시보드로 이동합니다.");
      navigate("/dashboard");
      return;
    }

    const fetchQuantitativeData = async () => {
      try {
        const response = await axios.get(
          "http://localhost:3000/selftest/quantitative",
          { params: { systemId }, withCredentials: true }
        );

        const data = response.data;
        setQuantitativeData(data);

        // ✅ 기존 피드백 존재 여부 확인 (다시 평가 시 표시)
        const hasExistingFeedback = data.some(
          (item) => item.feedback && item.feedback.trim() !== ""
        );
        setHasFeedback(hasExistingFeedback);

        // ✅ 기존 응답 데이터 설정
        const initialResponses = data.reduce((acc, item) => {
          acc[item.question_number] = {
            response: item.response || "",
            additionalComment: item.additional_comment || "",
            filePath: item.file_upload || null, // ✅ file_upload → filePath 변경
          };
          return acc;
        }, {});
        setResponses(initialResponses);

        console.log("✅ Initialized Responses:", data);
      } catch (error) {
        console.error("❌ Error fetching quantitative data:", error);
        alert("정량 데이터를 불러오는 데 실패했습니다. 다시 시도해주세요.");
      }
    };

    fetchQuantitativeData();
  }, [userId, systemId, navigate, setQuantitativeData, setResponses]);

  // 🔹 응답 저장 API 호출
  const saveAllResponses = async () => {
    if (!systemId || !userId || Object.keys(responses).length < 43) {
      alert("🚨 모든 문항에 응답해야 합니다.");
      return;
    }

    try {
      const formattedResponses = Object.entries(responses).map(
        ([question_number, responseData]) => ({
          systemId,
          userId,
          questionId: Number(question_number), // ✅ question_number → questionId 변경
          response: ["이행", "미이행", "해당없음", "자문 필요"].includes(
            responseData.response.trim()
          )
            ? responseData.response.trim()
            : "이행", // 기본값 설정
          additionalComment:
            responseData.response === "자문 필요" &&
            responseData.additionalComment
              ? responseData.additionalComment.trim()
              : "",
          filePath: responseData.filePath || null, // ✅ file_upload → filePath 변경
        })
      );

      console.log("📤 Sending quantitative responses:", formattedResponses);

      await axios.post(
        "http://localhost:3000/selftest/quantitative",
        { responses: formattedResponses },
        { withCredentials: true }
      );

      alert("✅ 정량 평가 응답이 저장되었습니다.");
      navigate("/qualitative-survey", { state: { systemId, userId } });
    } catch (error) {
      console.error("❌ 정량 평가 저장 실패:", error);
      alert("🚨 저장 중 오류 발생");
    }
  };

  const handleNextClick = async () => {
    if (currentStep < 43) {
      setCurrentStep((prev) => prev + 1);
    } else {
      await saveAllResponses();
    }
  };

  const handlePreviousClick = () => {
    if (currentStep > 1) setCurrentStep((prev) => prev - 1);
  };

  const renderCurrentStep = () => {
    const currentData = quantitativeData.find(
      (item) => item.question_number === currentStep
    ) || {
      question_number: currentStep,
      question: "질문 없음",
      evaluation_criteria: "N/A",
      legal_basis: "N/A",
      score: "N/A",
      filePath: null,
      additional_comment: "",
      feedback: "",
    };

    return (
      <table className="w-full border-collapse border border-gray-300 mb-6">
        <tbody>
          <tr>
            <td className="bg-gray-200 p-2 border">지표 번호</td>
            <td className="p-2 border">{currentData.question_number}</td>
            <td className="bg-gray-200 p-2 border">배점</td>
            <td className="p-2 border">{currentData.score}</td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">지표</td>
            <td colSpan="3" className="p-2 border">
              {currentData.question}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">근거법령</td>
            <td colSpan="3" className="p-2 border">
              {currentData.legal_basis}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">평가기준</td>
            <td colSpan="3" className="p-2 border">
              {currentData.evaluation_criteria}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">파일 첨부</td>
            <td colSpan="3" className="p-2 border">
              {currentData.filePath ? (
                <a
                  href={currentData.filePath}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-blue-500"
                >
                  첨부 파일 보기
                </a>
              ) : (
                <input type="file" className="w-full p-1 border rounded" />
              )}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">평가</td>
            <td colSpan="3" className="p-2 border">
              <select
                value={responses[currentStep]?.response || ""}
                onChange={(e) =>
                  setResponses((prev) => ({
                    ...prev,
                    [currentStep]: {
                      ...prev[currentStep],
                      response: e.target.value,
                    },
                  }))
                }
                className="w-full p-2 border border-gray-300 rounded-md"
              >
                <option value="이행">이행</option>
                <option value="미이행">미이행</option>
                <option value="해당없음">해당없음</option>
                <option value="자문 필요">자문 필요</option>
              </select>
            </td>
          </tr>
          {hasFeedback && currentData.feedback && (
            <tr>
              <td className="bg-gray-200 p-2 border">피드백</td>
              <td colSpan="3" className="p-2 border">
                {currentData.feedback}
              </td>
            </tr>
          )}
        </tbody>
      </table>
    );
  };

  return (
    <div className="bg-gray-100 min-h-screen flex flex-col items-center">
      <div className="container mx-auto max-w-5xl bg-white mt-10 p-6 rounded-lg shadow-lg">
        <h2 className="text-xl font-bold mb-6">정량 설문조사</h2>
        {renderCurrentStep()}
        <div className="flex justify-between mt-6">
          <button onClick={handlePreviousClick}>이전</button>
          <button onClick={handleNextClick}>
            {currentStep === 43 ? "완료" : "다음"}
          </button>
        </div>
      </div>
    </div>
  );
}

export default DiagnosisPage;

 

 

QualitativeSurvey.jsx

import React, { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import axios from "axios";
import { useRecoilState } from "recoil";
import {
  quantitativeDataState,
  currentStepState,
} from "../../state/selfTestState";
import { quantitativeFeedbackState } from "../../state/feedback";

function DiagnosisFeedbackPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const { systemId } = location.state || {};

  const [quantitativeData, setQuantitativeData] = useRecoilState(
    quantitativeDataState
  );
  const [feedbacks, setFeedbacks] = useRecoilState(quantitativeFeedbackState);
  const [currentStep, setCurrentStep] = useRecoilState(currentStepState);
  const [responses, setResponses] = useState({});

  useEffect(() => {
    if (!systemId) {
      console.error("System ID가 누락되었습니다.");
      alert("시스템 정보가 없습니다. 대시보드로 이동합니다.");
      navigate("/dashboard");
      return;
    }

    const fetchQuantitativeData = async () => {
      try {
        const response = await axios.get(
          "http://localhost:3000/selftest/quantitative",
          { params: { systemId }, withCredentials: true }
        );
        const data = response.data || [];
        setQuantitativeData(data);

        const initialResponses = data.reduce((acc, item) => {
          acc[item.question_number] = {
            response: item.response || "",
            feedback: item.feedback || "피드백 없음",
          };
          return acc;
        }, {});
        setResponses(initialResponses);
      } catch (error) {
        console.error("Error fetching quantitative data:", error);
        alert("데이터를 불러오는 중 오류가 발생했습니다.");
      }
    };

    fetchQuantitativeData();
  }, [systemId, navigate, setQuantitativeData]);

  const handleFeedbackChange = (questionNumber, value) => {
    setResponses((prev) => ({
      ...prev,
      [questionNumber]: {
        ...prev[questionNumber],
        feedback: value,
      },
    }));
  };

  const saveAllFeedbacks = async () => {
    const feedbackData = quantitativeData.map((item) => ({
      questionNumber: item.question_number,
      systemId,
      feedback: responses[item.question_number]?.feedback || "피드백 없음",
    }));

    console.log("Sending feedback data:", feedbackData);

    try {
      const response = await axios.post(
        "http://localhost:3000/selftest/quantitative/feedback",
        { systemId, feedbackResponses: feedbackData },
        { withCredentials: true }
      );

      alert(response.data.msg || "모든 피드백이 저장되었습니다.");
      console.log(
        "Navigating to /QualitativeSurveyfeedback with systemId:",
        systemId
      );

      // Navigate with systemId in state
      navigate("/QualitativeSurveyfeedback", { state: { systemId } });
    } catch (error) {
      console.error("Error saving feedback:", error.response?.data || error);
      alert(
        error.response?.data?.msg ||
          "피드백 저장 중 오류가 발생했습니다. 다시 시도해주세요."
      );
    }
  };

  const handleNextClick = () => {
    if (currentStep < 43) {
      setCurrentStep((prev) => prev + 1);
    } else {
      saveAllFeedbacks();
    }
  };

  const handlePreviousClick = () => {
    if (currentStep > 1) setCurrentStep((prev) => prev - 1);
  };

  const renderCurrentStep = () => {
    const currentData = quantitativeData.find(
      (item) => item.question_number === currentStep
    ) || {
      question_number: currentStep,
      unit: "N/A",
      evaluation_method: "N/A",
      score: "N/A",
      question: "질문 없음",
      legal_basis: "N/A",
      criteria_and_references: "N/A",
      file_upload: "",
      response: "",
      feedback: "피드백 없음",
    };

    return (
      <table className="w-full border-collapse border border-gray-300 mb-6">
        <tbody>
          <tr>
            <td className="bg-gray-200 p-2 border">지표 번호</td>
            <td className="p-2 border">{currentData.question_number}</td>
            <td className="bg-gray-200 p-2 border">단위</td>
            <td className="p-2 border">{currentData.unit}</td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">배점</td>
            <td className="p-2 border">{currentData.score}</td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">지표</td>
            <td colSpan="3" className="p-2 border">
              {currentData.question}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">근거법령</td>
            <td colSpan="3" className="p-2 border">
              {currentData.legal_basis}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">평가기준</td>
            <td colSpan="3" className="p-2 border">
              {currentData.evaluation_criteria}
            </td>
          </tr>

          <tr>
            <td className="bg-gray-200 p-2 border">파일 첨부</td>
            <td colSpan="3" className="p-2 border">
              {currentData.file_upload ? (
                <a
                  href={currentData.file_upload}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  첨부 파일 보기
                </a>
              ) : (
                "파일 없음"
              )}
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">기관회원 응답</td>
            <td colSpan="3" className="p-2 border">
              <input
                type="text"
                value={currentData.response || "응답 없음"}
                readOnly
                className="w-full p-2 border border-gray-300 bg-gray-100"
              />
            </td>
          </tr>
          <tr>
            <td className="bg-gray-200 p-2 border">피드백</td>
            <td colSpan="3" className="p-2 border">
              <textarea
                value={responses[currentStep]?.feedback || "피드백 없음"}
                onChange={(e) =>
                  handleFeedbackChange(currentStep, e.target.value)
                }
                className="w-full p-2 border border-gray-300 rounded-md"
                placeholder="피드백을 입력하세요"
              />
            </td>
          </tr>
        </tbody>
      </table>
    );
  };

  return (
    <div className="bg-gray-100 min-h-screen flex flex-col items-center">
      <div className="container mx-auto max-w-5xl bg-white mt-10 p-6 rounded-lg shadow-lg">
        <h2 className="text-xl font-bold mb-6">
          정량 피드백 작성 ({currentStep}/43)
        </h2>
        {renderCurrentStep()}
        <div className="flex justify-between mt-6">
          <button
            onClick={handlePreviousClick}
            disabled={currentStep === 1}
            className="px-6 py-2 bg-gray-400 text-white rounded-md shadow hover:bg-gray-500"
          >
            이전
          </button>
          <button
            onClick={handleNextClick}
            className="px-6 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
          >
            {currentStep === 43 ? "완료" : "다음"}
          </button>
        </div>
      </div>
    </div>
  );
}

export default DiagnosisFeedbackPage;

 

 

여기서 잠깐

다음 단계인 정성평가페이지에서는 8개에대한 평가를 마치고나면 정성응답저장 + 점수 및 등급 계산 → 자가진단결과페이지로의 이동이 이루어져야합니다.

테이블 구조(정량,정성 테이블 → 정량문항,정량응답,정성문항,정성응답)변경됨에 따라 기존의 점수 및 등급 계산에 대한 api에서 수정을 필요로합니다.

 

점수 및 등급 평가 관련 api routes/result.js 수정작업

🚨 정량(quantitative) & 정성(qualitative) 데이터 조회 문제

  • 기존 calculateAssessmentScore 함수에서 정량과 정성데이터를 조회하는 쿼리 존재
  • 하지만 정량(quantitative) 및 정성(qualitative) 테이블은 제거되었으며, quantitative_responses 및 qualitative_responses로 변경됨.
  • 따라서 calculateAssessmentScore의 쿼리를 변경해야 합니다.

 

수정된 calculateAssessmentScore

const calculateAssessmentScore = async (systemId) => {
  console.log("Calculating score for systemId:", systemId);

  // ✅ 변경된 테이블 구조 반영
  const queryQuantitative = `SELECT response FROM quantitative_responses WHERE system_id = ?`;
  const queryQualitative = `SELECT response FROM qualitative_responses WHERE system_id = ?`;

  try {
    const [quantitativeResults] = await pool.query(queryQuantitative, [systemId]);
    const [qualitativeResults] = await pool.query(queryQualitative, [systemId]);

    console.log("Quantitative results:", quantitativeResults);
    console.log("Qualitative results:", qualitativeResults);

    let score = 0;

    // ✅ 정량 평가 점수 계산
    quantitativeResults.forEach((item) => {
      if (item.response === "이행") score += 1;
      else if (item.response === "자문 필요") score += 0.3;
    });

    // ✅ 정성 평가 점수 계산
    qualitativeResults.forEach((item) => {
      if (item.response === "자문필요") score += 0.3;
    });

    console.log("Calculated score:", score);

    let grade = "D";
    if (score >= 80) grade = "S";
    else if (score >= 60) grade = "A";
    else if (score >= 40) grade = "B";
    else if (score >= 20) grade = "C";

    console.log("Calculated grade:", grade);

    return { score, grade };
  } catch (error) {
    console.error("점수 계산 실패:", error.message);
    throw error;
  }
};