Repository: biyard/voice-korea
Pull request #219: Feature/panel testing
Author: login: victor138128
Created: 2025-03-24T10:17:49Z | Merged: 2025-03-25T01:25:18Z

- survey testing and fix survey bugfix.
- implement authentication number check logic.

link: https://github.com/biyard/voice-korea/issues/218



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Survey questions now display provided descriptions for clearer content.
  - Membership creation now validates the authentication number format with immediate, localized error feedback.
  - Added an input field for entering extra question details in surveys.
  - Introduced a structured survey status system with clear UI indications based on the survey's progress.
  - Enhanced localization support with new translation entries for error messages and prompts.

- **Refactor**
  - Streamlined the attribute selection process in survey panels for a more intuitive user experience.

- **Documentation**
  - Expanded localization support with new error prompts and refined text entries, including an updated age range format.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.github/workflows/pr-workflow.yml b/.github/workflows/pr-workflow.yml
index 9828b847..4d39581f 100644
--- a/.github/workflows/pr-workflow.yml
+++ b/.github/workflows/pr-workflow.yml
@@ -87,7 +87,7 @@ jobs:
           SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
           SERVICE: main-ui
         run: |
-          npm i -g tailwindcss@3
+          npm i -g @tailwindcss/cli
           make build
 
   api:
diff --git a/.github/workflows/prod-workflow.yaml b/.github/workflows/prod-workflow.yaml
index f46eeeca..da6c187c 100644
--- a/.github/workflows/prod-workflow.yaml
+++ b/.github/workflows/prod-workflow.yaml
@@ -101,7 +101,7 @@ jobs:
         run: |
           cargo binstall dioxus-cli --force
           cargo binstall toml-cli --force
-          npm install -g aws-cdk tailwindcss@3
+          npm install -g aws-cdk @tailwindcss/cli
 
       - name: Deploy APIs
         env:
diff --git a/Cargo.lock b/Cargo.lock
index 8c727b9b..0ab3fe19 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -88,7 +88,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
 
 [[package]]
 name = "api"
-version = "0.3.23"
+version = "0.3.24"
 dependencies = [
  "aws-config",
  "aws-sdk-s3",
@@ -3335,7 +3335,7 @@ dependencies = [
 
 [[package]]
 name = "main-ui"
-version = "0.3.19"
+version = "0.3.20"
 dependencies = [
  "bdk",
  "by-components",
diff --git a/packages/api/Cargo.toml b/packages/api/Cargo.toml
index d638db5c..a2c32a5b 100644
--- a/packages/api/Cargo.toml
+++ b/packages/api/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "api"
-version = "0.3.23"
+version = "0.3.24"
 authors.workspace = true
 description.workspace = true
 edition.workspace = true
diff --git a/packages/api/src/utils/nonce_lab/create_survey_request.rs b/packages/api/src/utils/nonce_lab/create_survey_request.rs
index 7961dc2c..7d04b949 100644
--- a/packages/api/src/utils/nonce_lab/create_survey_request.rs
+++ b/packages/api/src/utils/nonce_lab/create_survey_request.rs
@@ -202,32 +202,42 @@ impl From<Question> for NonceLabSurveyQuestion {
     fn from(question: Question) -> Self {
         // NOTE: Noncelab API does not support description field for each question.
         match question {
-            Question::SingleChoice(ChoiceQuestion { title, options, .. }) => {
+            Question::SingleChoice(ChoiceQuestion {
+                title,
+                description,
+                options,
+                ..
+            }) => NonceLabSurveyQuestion {
+                title: title.clone(),
+                question: NonceLabSurveyQuestionType::SingleChoice {
+                    question: description.unwrap_or_default(),
+                    options,
+                },
+            },
+            Question::MultipleChoice(ChoiceQuestion {
+                title,
+                description,
+                options,
+                ..
+            }) => NonceLabSurveyQuestion {
+                title: title.clone(),
+                question: NonceLabSurveyQuestionType::MultipleChoice {
+                    question: description.unwrap_or_default(),
+                    options,
+                },
+            },
+            Question::ShortAnswer(SubjectiveQuestion { title, description }) => {
                 NonceLabSurveyQuestion {
                     title: title.clone(),
-                    question: NonceLabSurveyQuestionType::SingleChoice {
-                        question: title,
-                        options,
-                    },
+                    question: NonceLabSurveyQuestionType::Text(description),
                 }
             }
-            Question::MultipleChoice(ChoiceQuestion { title, options, .. }) => {
+            Question::Subjective(SubjectiveQuestion { title, description }) => {
                 NonceLabSurveyQuestion {
                     title: title.clone(),
-                    question: NonceLabSurveyQuestionType::MultipleChoice {
-                        question: title,
-                        options,
-                    },
+                    question: NonceLabSurveyQuestionType::LongText(description),
                 }
             }
-            Question::ShortAnswer(SubjectiveQuestion { title, .. }) => NonceLabSurveyQuestion {
-                title: title.clone(),
-                question: NonceLabSurveyQuestionType::Text(title),
-            },
-            Question::Subjective(SubjectiveQuestion { title, .. }) => NonceLabSurveyQuestion {
-                title: title.clone(),
-                question: NonceLabSurveyQuestionType::LongText(title),
-            },
         }
     }
 }
diff --git a/packages/main-ui/Cargo.toml b/packages/main-ui/Cargo.toml
index face6f2c..05e15e16 100644
--- a/packages/main-ui/Cargo.toml
+++ b/packages/main-ui/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "main-ui"
-version = "0.3.19"
+version = "0.3.20"
 authors.workspace = true
 edition.workspace = true
 
diff --git a/packages/main-ui/Makefile b/packages/main-ui/Makefile
index 36737505..7ad9eaea 100644
--- a/packages/main-ui/Makefile
+++ b/packages/main-ui/Makefile
@@ -24,7 +24,7 @@ setup.tool:
 	cargo install cargo-binstall
 	cargo binstall dioxus-cli
 	cargo binstall toml-cli
-	npm i -g tailwindcss@3
+	npm i -g @tailwindcss/cli
 
 run: public/tailwind.css
 	$(BUILD_ENV) dx serve
@@ -33,7 +33,7 @@ clean:
 	rm -rf public/tailwind.css
 
 public/tailwind.css:
-	tailwindcss -i ./input.css -o ./public/tailwind.css
+	tailwindcss -i ./public/input.css -o ./public/tailwind.css
 
 build: clean public/tailwind.css
 	$(BUILD_ENV) dx build --release --fullstack --server-features lambda
diff --git a/packages/main-ui/input.css b/packages/main-ui/input.css
deleted file mode 100644
index bd6213e1..00000000
--- a/packages/main-ui/input.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
\ No newline at end of file
diff --git a/packages/main-ui/public/input.css b/packages/main-ui/public/input.css
new file mode 100644
index 00000000..cf71eaa1
--- /dev/null
+++ b/packages/main-ui/public/input.css
@@ -0,0 +1,9 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@theme static {
+  /* App-specific colors */
+  --color-error: #FF0000;
+  --color-gray-border: #E0E0E0;
+}
\ No newline at end of file
diff --git a/packages/main-ui/src/main.rs b/packages/main-ui/src/main.rs
index 45b5f8b9..641d4307 100644
--- a/packages/main-ui/src/main.rs
+++ b/packages/main-ui/src/main.rs
@@ -34,6 +34,8 @@ fn App() -> Element {
     OpinionApi::init();
     MetadataApi::init();
 
+    let css = include_str!("../public/input.css");
+
     rsx! {
         document::Link {
             rel: "icon",
@@ -52,27 +54,12 @@ fn App() -> Element {
             rel: "stylesheet",
         }
         document::Script { src: "https://d3js.org/d3.v7.min.js" }
+        document::Style { r#type: "text/tailwindcss", {css} }
+        document::Script { src: "https://unpkg.com/@tailwindcss/browser@4.0.12/dist/index.global.js" }
+        document::Style { href: asset!("/public/tailwind.css") }
+        document::Style { href: asset!("/public/main.css") }
 
-        head {
-            link { rel: "stylesheet", href: asset!("/public/main.css") }
-            link { rel: "stylesheet", href: asset!("/public/tailwind.css") }
-            load_tailwindcss {}
-        }
         HoverEffects {}
         Router::<Route> {}
     }
 }
-
-#[cfg(not(feature = "lambda"))]
-#[allow(dead_code)]
-fn load_tailwindcss() -> Element {
-    rsx! {
-        script { src: "https://cdn.tailwindcss.com/3.4.5" }
-    }
-}
-
-#[cfg(feature = "lambda")]
-#[allow(dead_code)]
-fn load_tailwindcss() -> Element {
-    rsx! {}
-}
diff --git a/packages/main-ui/src/pages/create/controller.rs b/packages/main-ui/src/pages/create/controller.rs
index 080f2218..eb540dae 100644
--- a/packages/main-ui/src/pages/create/controller.rs
+++ b/packages/main-ui/src/pages/create/controller.rs
@@ -36,6 +36,7 @@ pub struct Controller {
     password_pattern_error: Signal<bool>,
     invalid_authkey_error: Signal<bool>,
     already_exists_user_error: Signal<bool>,
+    auth_error: Signal<bool>,
     unknown_error: Signal<bool>,
     // click_send_authentication: Signal<bool>,
     // click_search_address: Signal<bool>,
@@ -78,6 +79,7 @@ impl Controller {
             invalid_authkey_error: use_signal(|| false),
             already_exists_user_error: use_signal(|| false),
             unknown_error: use_signal(|| false),
+            auth_error: use_signal(|| false),
 
             auth_key: use_signal(|| "".to_string()),
             auth_api,
@@ -177,6 +179,10 @@ impl Controller {
         (self.invalid_authkey_error)()
     }
 
+    pub fn get_authorization_error(&self) -> bool {
+        (self.auth_error)()
+    }
+
     pub fn get_already_exists_user_error(&self) -> bool {
         (self.already_exists_user_error)()
     }
@@ -310,10 +316,14 @@ impl Controller {
     }
 
     pub async fn set_click_complete_join_membership(&mut self) {
+        let re = Regex::new(r"^[a-zA-Z0-9+\-_.]{6}$").unwrap();
+
         let mut has_number = false;
         let mut has_special = false;
         let mut has_alpha = false;
 
+        let mut has_error = false;
+
         for c in self.get_password().chars() {
             if c.is_numeric() {
                 has_number = true;
@@ -323,20 +333,32 @@ impl Controller {
                 has_special = true;
             }
         }
+
+        if !re.is_match(&self.get_authentication_number().as_str()) {
+            self.auth_error.set(true);
+            has_error = true;
+        }
         if self.get_password().is_empty() {
             self.password_error.set(true);
-            return;
-        } else if self.get_password() != self.get_password_check() {
+            has_error = true;
+        }
+        if self.get_password() != self.get_password_check() {
             self.password_check_error.set(true);
-            return;
-        } else if !has_number || !has_special || !has_alpha {
+            has_error = true;
+        }
+        if !has_number || !has_special || !has_alpha {
             self.password_pattern_error.set(true);
+            has_error = true;
+        }
+
+        if has_error {
             return;
         }
 
         self.password_error.set(false);
         self.password_check_error.set(false);
         self.password_pattern_error.set(false);
+        self.auth_error.set(false);
         let res = self
             .user_api
             .read()
diff --git a/packages/main-ui/src/pages/create/i18n.rs b/packages/main-ui/src/pages/create/i18n.rs
index 73f802bc..385ec080 100644
--- a/packages/main-ui/src/pages/create/i18n.rs
+++ b/packages/main-ui/src/pages/create/i18n.rs
@@ -204,4 +204,8 @@ translate! {
         ko: "영문, 숫자, 특수기호 조합으로 8자 이상 구성해주세요.",
         en: "Please make up at least 8 characters using a combination of letters, numbers, and special symbols."
     },
+    invalid_authorization_pattern: {
+        ko: "인증번호 형태가 올바르지 않습니다.",
+        en: "The authentication number format is incorrect."
+    }
 }
diff --git a/packages/main-ui/src/pages/create/page.rs b/packages/main-ui/src/pages/create/page.rs
index 74b41a30..48c0e9db 100644
--- a/packages/main-ui/src/pages/create/page.rs
+++ b/packages/main-ui/src/pages/create/page.rs
@@ -138,6 +138,7 @@ pub fn CreatePage(props: CreatePageProps) -> Element {
                     ],
                     complete_join_membership: translates.complete_join_membership,
 
+                    invalid_authorization_pattern: translates.invalid_authorization_pattern,
                     invalid_password_pattern: translates.invalid_password_pattern,
                     incollect_email_form: translates.incollect_email_form,
                     input_password: translates.input_password,
diff --git a/packages/main-ui/src/pages/create/step_three.rs b/packages/main-ui/src/pages/create/step_three.rs
index 9912a692..b12df6c4 100644
--- a/packages/main-ui/src/pages/create/step_three.rs
+++ b/packages/main-ui/src/pages/create/step_three.rs
@@ -28,6 +28,7 @@ pub struct StepThreeProps {
     check_membership_descriptions: Vec<String>,
     complete_join_membership: String,
 
+    invalid_authorization_pattern: String,
     invalid_password_pattern: String,
     incollect_email_form: String,
     input_password: String,
@@ -86,6 +87,13 @@ pub fn StepThreePage(props: StepThreeProps) -> Element {
                                     onchange: move |e| {
                                         ctrl.set_authentication_number(e);
                                     },
+                                    border: if ctrl.get_authorization_error() { "border-error" } else { "border-gray-border" },
+                                }
+
+                                if ctrl.get_authorization_error() {
+                                    div { class: "mt-[10px] font-normal text-[#ff0000] text-[12px]",
+                                        {props.invalid_authorization_pattern}
+                                    }
                                 }
                             }
                             div { class: "text-[16px] font-normal text-[#636363]", "{props.authentication_descriptions[0]}" }
@@ -145,94 +153,6 @@ pub fn StepThreePage(props: StepThreeProps) -> Element {
                         }
                     },
                 }
-                        // Row {
-            //     enable_bottom_border: false,
-            //     label: props.company_info,
-            //     element: rsx! {
-            //         div {
-            //             class: "flex flex-row w-full h-full items-center mx-[20px] text-black font-normal text-[16px]",
-            //             "{props.company_example}"
-            //         }
-            //     }
-            // }
-            // Row {
-            //     enable_bottom_border: false,
-            //     label: props.name_info,
-            //     element: rsx! {
-            //         div {
-            //             class: "flex flex-row w-full h-full justify-start items-center",
-            //             div {
-            //                 class: "mx-[10px]",
-            //                 Input {
-            //                     value: ctrl.get_name(),
-            //                     placeholder: props.name_example,
-            //                     onchange: move |e| {
-            //                         ctrl.set_name(e);
-            //                     }
-            //                 }
-            //             },
-            //         }
-            //     }
-            // }
-            // Row {
-            //     enable_bottom_border: false,
-            //     label: props.phone_info,
-            //     element: rsx! {
-            //         div {
-            //             class: "flex flex-row w-full h-full justify-start items-center",
-            //             div {
-            //                 class: "mx-[10px]",
-            //                 Input {
-            //                     value: ctrl.get_cellphone_number(),
-            //                     placeholder: props.phone_example,
-            //                     onchange: move |e| {
-            //                         ctrl.set_cellphone_number(e);
-            //                     }
-            //                 }
-            //             },
-            //         }
-            //     }
-            // }
-            // Row {
-            //     enable_bottom_border: true,
-            //     height: Some(135),
-            //     label: props.address_info,
-            //     element: rsx! {
-            //         div {
-            //             class: "flex flex-col w-full h-min justify-start items-start mt-[10px]",
-            //             div {
-            //                 class: "flex flex-row w-full h-full justify-start items-center mb-[10px]",
-            //                 div {
-            //                     class: "mx-[10px]",
-            //                     Input {
-            //                         value: ctrl.get_simple_address(),
-            //                         onchange: move |e| {
-            //                             ctrl.set_simple_address(e);
-            //                         }
-            //                     }
-            //                 },
-            //                 ButtonComponent {
-            //                     width: Some(130),
-            //                     label: props.search_address,
-            //                     lang: props.lang,
-            //                     onclick: move |_| {
-            //                         ctrl.set_click_search_address();
-            //                     }
-            //                 }
-            //             }
-            //             div {
-            //                 class: "flex flex-row w-full mx-[10px] pr-[20px]",
-            //                 Input {
-            //                     value: ctrl.get_detail_address(),
-            //                     onchange: move |e| {
-            //                         ctrl.set_detail_address(e);
-            //                     },
-            //                     width: Some(-1)
-            //                 }
-            //             },
-            //         }
-            //     }
-            // }
             }
             if ctrl.get_already_exists_user_error() {
                 div { class: "flex flex-col w-full h-min min-w-[710px] px-[20px] py-[15px]",
diff --git a/packages/main-ui/src/pages/surveys/components/create_panel_modal.rs b/packages/main-ui/src/pages/surveys/components/create_panel_modal.rs
index ed646569..fbf07d55 100644
--- a/packages/main-ui/src/pages/surveys/components/create_panel_modal.rs
+++ b/packages/main-ui/src/pages/surveys/components/create_panel_modal.rs
@@ -1,11 +1,12 @@
+use std::str::FromStr;
+
 use dioxus::prelude::*;
 use dioxus_logger::tracing;
 use dioxus_translate::{translate, Language};
-use models::{
-    attribute_v2::{GenderV2, RegionV2, SalaryV2},
-    response::AgeV3,
-    PanelV2CreateRequest,
-};
+use models::attribute_v2::GenderV2;
+use models::attribute_v2::RegionV2;
+use models::attribute_v2::SalaryV2;
+use models::{response::AgeV3, PanelV2CreateRequest};
 
 use crate::{
     components::icons::{Clear, Remove},
@@ -29,79 +30,14 @@ pub struct AttributeInfo {
 pub fn CreatePanelModal(props: CreatePanelModalProps) -> Element {
     let translate: CreatePanelModalTranslate = translate(&props.lang);
     let mut panel_name: Signal<String> = use_signal(|| "".to_string());
-    let mut selected_value: Signal<Vec<String>> = use_signal(|| {
-        vec![
-            "".to_string(),
-            "".to_string(),
-            "".to_string(),
-            "".to_string(),
-        ]
-    });
     let mut is_open: Signal<Vec<bool>> = use_signal(|| vec![false, false, false, false]);
 
     let panel_name_error: Signal<String> = use_signal(|| "".to_string());
-    let error_signals: Vec<Signal<String>> = vec![
-        use_signal(|| "".to_string()), // age_error
-        use_signal(|| "".to_string()), // gender_error
-        use_signal(|| "".to_string()), // region_error
-        use_signal(|| "".to_string()), // salary_error
-    ];
 
-    let total_attributes: Signal<Vec<AttributeInfo>> = use_signal(|| {
-        vec![
-            AttributeInfo {
-                name: translate.clone().age.to_string(),
-                values: vec![
-                    translate.clone().teenager.to_string(),
-                    translate.clone().twenty.to_string(),
-                    translate.clone().thirty.to_string(),
-                    translate.clone().fourty.to_string(),
-                    translate.clone().fifty.to_string(),
-                    translate.clone().sixty.to_string(),
-                    translate.clone().over.to_string(),
-                ],
-            },
-            AttributeInfo {
-                name: translate.clone().gender.to_string(),
-                values: vec![
-                    translate.clone().male.to_string(),
-                    translate.clone().female.to_string(),
-                ],
-            },
-            AttributeInfo {
-                name: translate.clone().region.to_string(),
-                values: vec![
-                    translate.clone().seoul.to_string(),
-                    translate.clone().busan.to_string(),
-                    translate.clone().daegu.to_string(),
-                    translate.clone().incheon.to_string(),
-                    translate.clone().gwangju.to_string(),
-                    translate.clone().daejeon.to_string(),
-                    translate.clone().ulsan.to_string(),
-                    translate.clone().sejong.to_string(),
-                    translate.clone().gyeongi.to_string(),
-                    translate.clone().gangwon.to_string(),
-                    translate.clone().chungbuk.to_string(),
-                    translate.clone().chungnam.to_string(),
-                    translate.clone().jeonbuk.to_string(),
-                    translate.clone().jeonnam.to_string(),
-                    translate.clone().gyeonbuk.to_string(),
-                    translate.clone().gyeonnam.to_string(),
-                    translate.clone().jeju.to_string(),
-                ],
-            },
-            AttributeInfo {
-                name: translate.clone().salary.to_string(),
-                values: vec![
-                    translate.clone().tier_one.to_string(),
-                    translate.clone().tier_two.to_string(),
-                    translate.clone().tier_three.to_string(),
-                    translate.clone().tier_four.to_string(),
-                    translate.clone().tier_five.to_string(),
-                ],
-            },
-        ]
-    });
+    let mut selected_ages: Signal<Vec<String>> = use_signal(|| vec![]);
+    let mut selected_genders: Signal<Vec<String>> = use_signal(|| vec![]);
+    let mut selected_regions: Signal<Vec<String>> = use_signal(|| vec![]);
+    let mut selected_salarys: Signal<Vec<String>> = use_signal(|| vec![]);
 
     rsx! {
         div { class: "flex flex-col w-[540px] justify-start items-start",
@@ -129,142 +65,142 @@ pub fn CreatePanelModal(props: CreatePanelModalProps) -> Element {
                 }
             }
 
-            div { class: "flex flex-col w-full justify-start items-start bg-white border border-[#bfc8d9] rounded-[8px] p-[24px]",
+            div { class: "flex flex-col w-full justify-start items-start gap-[10px]",
+                SelectedAttribute {
+                    label: translate.age,
+                    is_open: (is_open)()[0],
+                    set_open: move |open: bool| {
+                        let mut opens = is_open();
+                        opens[0] = open;
+                        is_open.set(opens);
+                    },
 
-                for (index , attribute) in total_attributes().iter().enumerate() {
-                    div { class: "flex flex-row w-full justify-start items-center gap-[10px] mt-[10px]",
-                        div { class: "w-[50px] font-medium text-[#222222] text-[15px]",
-                            "{attribute.name}"
-                        }
-                        div { class: "relative w-full",
-                            div { class: "flex flex-col w-full justify-start items-start",
-                                button {
-                                    class: "flex flex-row w-full justify-start items-center bg-[#f7f7f7] rounded-[4px] p-[15px] min-h-[55px]",
-                                    onclick: move |_| {
-                                        let mut open = is_open();
-                                        open[index] = true;
-                                        is_open.set(open);
-                                    },
-                                    if selected_value()[index] != "" {
-                                        AttributeLabel {
-                                            label: selected_value()[index].clone(),
-                                            onclose: move |e: Event<MouseData>| {
-                                                e.stop_propagation();
-                                                e.prevent_default();
-                                                let mut attributes = selected_value();
-                                                attributes[index] = "".to_string();
-                                                selected_value.set(attributes);
-                                            },
-                                        }
-                                    }
-                                }
+                    total_attributes: vec![
+                        translate.clone().teenager.to_string(),
+                        translate.clone().twenty.to_string(),
+                        translate.clone().thirty.to_string(),
+                        translate.clone().fourty.to_string(),
+                        translate.clone().fifty.to_string(),
+                        translate.clone().sixty.to_string(),
+                        translate.clone().over.to_string(),
+                    ],
+                    selected_attributes: selected_ages(),
+                    change_attributes: move |attrs: Vec<String>| {
+                        selected_ages.set(attrs);
+                    },
+                }
+                SelectedAttribute {
+                    label: translate.gender,
+                    is_open: (is_open)()[1],
+                    set_open: move |open: bool| {
+                        let mut opens = is_open();
+                        opens[1] = open;
+                        is_open.set(opens);
+                    },
 
-                                if error_signals[index]() != "" {
-                                    div { class: "font-semibold text-red-600 text-[13px] mt-[10px]",
-                                        {error_signals[index]()}
-                                    }
-                                }
-                            }
+                    total_attributes: vec![translate.clone().male.to_string(), translate.clone().female.to_string()],
+                    selected_attributes: selected_genders(),
+                    change_attributes: move |attrs: Vec<String>| {
+                        selected_genders.set(attrs);
+                    },
+                }
+                SelectedAttribute {
+                    label: translate.region,
+                    is_open: (is_open)()[2],
+                    set_open: move |open: bool| {
+                        let mut opens = is_open();
+                        opens[2] = open;
+                        is_open.set(opens);
+                    },
 
-                            if is_open()[index] {
-                                div { class: "absolute flex flex-col w-full justify-start items-center shadow-[0px_8px_20px_rgba(20,26,62,0.25)] bg-white py-4 rounded-md z-20",
-                                    div { class: "flex flex-row w-full justify-end px-[10px]",
-                                        button {
-                                            onclick: move |e: Event<MouseData>| {
-                                                e.stop_propagation();
-                                                e.prevent_default();
-                                                let mut open = is_open();
-                                                open[index] = false;
-                                                is_open.set(open);
-                                            },
-                                            Remove {
-                                                width: "15",
-                                                height: "15",
-                                                fill: "#555462",
-                                            }
-                                        }
-                                    }
+                    total_attributes: vec![
+                        translate.clone().seoul.to_string(),
+                        translate.clone().busan.to_string(),
+                        translate.clone().daegu.to_string(),
+                        translate.clone().incheon.to_string(),
+                        translate.clone().gwangju.to_string(),
+                        translate.clone().daejeon.to_string(),
+                        translate.clone().ulsan.to_string(),
+                        translate.clone().sejong.to_string(),
+                        translate.clone().gyeongi.to_string(),
+                        translate.clone().gangwon.to_string(),
+                        translate.clone().chungbuk.to_string(),
+                        translate.clone().chungnam.to_string(),
+                        translate.clone().jeonbuk.to_string(),
+                        translate.clone().jeonnam.to_string(),
+                        translate.clone().gyeonbuk.to_string(),
+                        translate.clone().gyeonnam.to_string(),
+                        translate.clone().jeju.to_string(),
+                    ],
+                    selected_attributes: selected_regions(),
+                    change_attributes: move |attrs: Vec<String>| {
+                        selected_regions.set(attrs);
+                    },
+                }
+                SelectedAttribute {
+                    label: translate.salary,
+                    is_open: (is_open)()[3],
+                    set_open: move |open: bool| {
+                        let mut opens = is_open();
+                        opens[3] = open;
+                        is_open.set(opens);
+                    },
 
-                                    div { class: "flex flex-col w-full max-h-[150px] overflow-y-auto justify-start items-start",
-                                        for value in attribute.values.clone() {
-                                            div {
-                                                class: "flex flex-col w-full h-[60px] justify-start items-start py-[9px] bg-white hover:bg-[#f7f7f7] hover:border-l hover:border-l-[#2a60d3] cursor-pointer",
-                                                onclick: move |_| {
-                                                    tracing::info!("attribute value: {}", value);
-                                                    let mut values = selected_value();
-                                                    values[index] = value.clone();
-                                                    selected_value.set(values);
-                                                    let mut open = is_open();
-                                                    open[index] = false;
-                                                    is_open.set(open);
-                                                },
-                                                div { class: "flex flex-col w-full px-4",
-                                                    div { class: "font-bold text-[15px] text-[#222222] mb-[5px]",
-                                                        "{value}"
-                                                    }
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
+                    total_attributes: vec![
+                        translate.clone().tier_one.to_string(),
+                        translate.clone().tier_two.to_string(),
+                        translate.clone().tier_three.to_string(),
+                        translate.clone().tier_four.to_string(),
+                        translate.clone().tier_five.to_string(),
+                    ],
+                    selected_attributes: selected_salarys(),
+                    change_attributes: move |attrs: Vec<String>| {
+                        selected_salarys.set(attrs);
+                    },
                 }
             }
 
             div { class: "flex flex-row w-full justify-start items-center gap-[20px] mt-[40px]",
-                //FIXME: add error handling
                 button {
                     class: "flex flex-row bg-[#2a60d3] rounded-[4px] px-[14px] py-[8px] font-semibold text-white text-[16px] leading-[24px]",
                     onclick: {
-                        let values = selected_value();
-                        let translate = translate.clone();
-                        let mut age: Option<AgeV3> = None;
-                        let mut gender: Option<GenderV2> = None;
-                        let mut region: Option<RegionV2> = None;
-                        let mut salary: Option<SalaryV2> = None;
-                        if values[0] != "" && values[1] != "" && values[2] != "" && values[3] != "" {
-                            age = Some(values[0].parse().unwrap());
-                            gender = Some(values[1].parse().unwrap());
-                            region = Some(values[2].parse().unwrap());
-                            salary = Some(values[3].parse().unwrap());
-                        }
+                        let selected_ages = (selected_ages)();
+                        let selected_genders = (selected_genders)();
+                        let selected_regions = (selected_regions)();
+                        let selected_salarys = (selected_salarys)();
                         move |_| {
-                            if check_condition(
-                                translate.clone(),
-                                panel_name_error,
-                                error_signals[0],
-                                error_signals[1],
-                                error_signals[2],
-                                error_signals[3],
-                                panel_name(),
-                                values[0].clone(),
-                                values[1].clone(),
-                                values[2].clone(),
-                                values[3].clone(),
-                            ) {
-                                props
-                                    .onsave
-                                    .call(PanelV2CreateRequest {
-                                        name: panel_name(),
-                                        user_count: 0,
-                                        attributes: vec![
-                                            models::response::Attribute::Age(
-                                                age.clone().unwrap_or_default(),
-                                            ),
-                                            models::response::Attribute::Gender(
-                                                gender.clone().unwrap_or_default(),
-                                            ),
-                                            models::response::Attribute::Region(
-                                                region.clone().unwrap_or_default(),
-                                            ),
-                                            models::response::Attribute::Salary(
-                                                salary.clone().unwrap_or_default(),
-                                            ),
-                                        ],
-                                    });
+                            let mut attributes = vec![];
+                            for age in selected_ages.clone() {
+                                let age = models::prelude::response::Attribute::Age(
+                                    AgeV3::from_str(&age).unwrap_or_default(),
+                                );
+                                attributes.push(age);
+                            }
+                            for gender in selected_genders.clone() {
+                                let gender = models::prelude::response::Attribute::Gender(
+                                    GenderV2::from_str(&gender).unwrap_or_default(),
+                                );
+                                attributes.push(gender);
+                            }
+                            for region in selected_regions.clone() {
+                                let region = models::prelude::response::Attribute::Region(
+                                    RegionV2::from_str(&region).unwrap_or_default(),
+                                );
+                                attributes.push(region);
+                            }
+                            for salary in selected_salarys.clone() {
+                                let salary = models::prelude::response::Attribute::Salary(
+                                    SalaryV2::from_str(&salary).unwrap_or_default(),
+                                );
+                                attributes.push(salary);
                             }
+                            props
+                                .onsave
+                                .call(PanelV2CreateRequest {
+                                    name: panel_name(),
+                                    user_count: 0,
+                                    attributes,
+                                });
                         }
                     },
                     "{translate.save}"
@@ -281,6 +217,94 @@ pub fn CreatePanelModal(props: CreatePanelModalProps) -> Element {
     }
 }
 
+#[component]
+pub fn SelectedAttribute(
+    label: String,
+    is_open: bool,
+    set_open: EventHandler<bool>,
+
+    total_attributes: Vec<String>,
+    selected_attributes: Vec<String>,
+    change_attributes: EventHandler<Vec<String>>,
+) -> Element {
+    rsx! {
+        div { class: "flex flex-row w-full justify-start items-center gap-[10px] mt-[10px]",
+            div { class: "w-[50px] font-medium text-[#222222] text-[15px]", "{label}" }
+            div { class: "relative w-full",
+                div { class: "flex flex-col w-full justify-start items-start gap-[10px]",
+                    button {
+                        class: "flex flex-row w-full justify-start items-center bg-[#f7f7f7] rounded-[4px] p-[15px] min-h-[55px]",
+                        onclick: move |_| {
+                            set_open.call(!is_open);
+                        },
+
+                        div { class: "flex flex-wrap flex-1 justify-start items-start gap-[10px]",
+                            for selected_attribute in selected_attributes.clone() {
+                                AttributeLabel {
+                                    label: selected_attribute.clone(),
+                                    onclose: {
+                                        let selected_attributes = selected_attributes.clone();
+                                        move |e: Event<MouseData>| {
+                                            e.stop_propagation();
+                                            e.prevent_default();
+                                            let mut attrs = selected_attributes.clone();
+                                            attrs.retain(|v| !(*v == selected_attribute));
+                                            change_attributes.call(attrs);
+                                        }
+                                    },
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if is_open {
+                    div { class: "absolute flex flex-col w-full justify-start items-center shadow-[0px_8px_20px_rgba(20,26,62,0.25)] bg-white py-4 rounded-md z-20",
+                        div { class: "flex flex-row w-full justify-end px-[10px]",
+                            button {
+                                onclick: move |_| {
+                                    set_open.call(false);
+                                },
+                                Remove {
+                                    width: "15",
+                                    height: "15",
+                                    fill: "#555462",
+                                }
+                            }
+                        }
+
+                        div { class: "flex flex-col w-full max-h-[150px] overflow-y-auto justify-start items-start",
+                            for value in total_attributes.clone() {
+                                if !selected_attributes.contains(&value) {
+                                    div {
+                                        class: "flex flex-col w-full h-[60px] justify-start items-start py-[9px] bg-white hover:bg-[#f7f7f7] hover:border-l hover:border-l-[#2a60d3] cursor-pointer",
+                                        onclick: {
+                                            let selected_attributes = selected_attributes.clone();
+                                            let attribute = value.clone();
+                                            move |_| {
+                                                tracing::info!("attribute value: {}", value);
+                                                let mut attrs = selected_attributes.clone();
+                                                attrs.push(attribute.clone());
+                                                change_attributes.call(attrs);
+                                                set_open.call(false);
+                                            }
+                                        },
+                                        div { class: "flex flex-col w-full px-4",
+                                            div { class: "font-bold text-[15px] text-[#222222] mb-[5px]",
+                                                "{value}"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
 #[component]
 pub fn AttributeLabel(label: String, onclose: EventHandler<MouseEvent>) -> Element {
     rsx! {
@@ -295,56 +319,3 @@ pub fn AttributeLabel(label: String, onclose: EventHandler<MouseEvent>) -> Eleme
         }
     }
 }
-
-pub fn check_condition(
-    tr: CreatePanelModalTranslate,
-
-    mut panel_name_error: Signal<String>,
-    mut age_error: Signal<String>,
-    mut gender_error: Signal<String>,
-    mut region_error: Signal<String>,
-    mut salary_error: Signal<String>,
-
-    panel_name: String,
-    age: String,
-    gender: String,
-    region: String,
-    salary: String,
-) -> bool {
-    if panel_name.len() < 2 {
-        panel_name_error.set(tr.panel_name_error.to_string());
-        return false;
-    } else {
-        panel_name_error.set("".to_string());
-    }
-
-    if age == "" {
-        age_error.set(tr.age_error.to_string());
-        return false;
-    } else {
-        age_error.set("".to_string());
-    }
-
-    if gender == "" {
-        gender_error.set(tr.gender_error.to_string());
-        return false;
-    } else {
-        gender_error.set("".to_string());
-    }
-
-    if region == "" {
-        region_error.set(tr.region_error.to_string());
-        return false;
-    } else {
-        region_error.set("".to_string());
-    }
-
-    if salary == "" {
-        salary_error.set(tr.salary_error.to_string());
-        return false;
-    } else {
-        salary_error.set("".to_string());
-    }
-
-    true
-}
diff --git a/packages/main-ui/src/pages/surveys/components/survey.rs b/packages/main-ui/src/pages/surveys/components/survey.rs
index 768be206..03d13521 100644
--- a/packages/main-ui/src/pages/surveys/components/survey.rs
+++ b/packages/main-ui/src/pages/surveys/components/survey.rs
@@ -11,20 +11,12 @@ use crate::{
         new::i18n::AddQuestionTranslate,
     },
 };
-// onchange: move |(index, survey): (usize, Question)| {
-//     questions.with_mut(move |q| q[index] = survey);
-// },
-// onremove: move |index: usize| {
-//     questions.remove(index);
-// },
 
 #[component]
 pub fn QuestionListView(
     lang: Language,
     questions: Signal<Vec<Question>>,
     onchange: EventHandler<Vec<Question>>,
-    // onchange: EventHandler<(usize, Question)>,
-    // onremove: EventHandler<usize>,
 ) -> Element {
     let tr: QuestionListViewTranslate = translate(&lang);
 
@@ -94,7 +86,6 @@ pub fn QuestionListView(
                         }
                     }
                 }
-            
             }
         }
 
@@ -135,6 +126,23 @@ pub fn Objective(
 
     rsx! {
         div { class: "flex flex-col w-full justify-start items-start",
+
+            input {
+                class: format!(
+                    "flex flex-row w-full h-[55px] justify-start items-center bg-[#f7f7f7] focus:outline-none px-[15px] py-[10px] font-medium text-[#b4b4b4] text-[15px] leading-[22px] rounded-[4px] mt-[10px]",
+                ),
+                r#type: "text",
+                placeholder: tr.input_description,
+                value: question.description(),
+                oninput: {
+                    let mut question = question.clone();
+                    move |e: Event<FormData>| {
+                        question.set_description(&e.value());
+                        onchange.call(question.clone());
+                    }
+                },
+            }
+
             div { class: "flex flex-row w-full h-[1px] bg-[#ebeff5] my-[10px]" }
 
             for (index , option) in options.iter().enumerate() {
diff --git a/packages/main-ui/src/pages/surveys/i18n.rs b/packages/main-ui/src/pages/surveys/i18n.rs
index 8e5460db..b0779d01 100644
--- a/packages/main-ui/src/pages/surveys/i18n.rs
+++ b/packages/main-ui/src/pages/surveys/i18n.rs
@@ -91,6 +91,10 @@ translate! {
         ko: "삭제",
         en: "Remove"
     }
+    input_description: {
+        ko: "설명을 입력해주세요.",
+        en: "Please Input Description"
+    }
 }
 
 translate! {
@@ -155,7 +159,7 @@ translate! {
         en: "Under 17 years old"
     }
     twenty: {
-        ko: "18~29세",
+        ko: "18-29세",
         en: "18-29 years old"
     }
     thirty: {
diff --git a/packages/main-ui/src/pages/surveys/page.rs b/packages/main-ui/src/pages/surveys/page.rs
index 2b1bd4a9..5a71d419 100644
--- a/packages/main-ui/src/pages/surveys/page.rs
+++ b/packages/main-ui/src/pages/surveys/page.rs
@@ -1,5 +1,5 @@
-use dioxus::prelude::*;
-use dioxus_translate::{translate, Language};
+#![allow(non_snake_case, dead_code, unused_variables)]
+use bdk::prelude::*;
 use models::ProjectStatus;
 
 use crate::{
@@ -187,7 +187,7 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                         }
                                         div { class: "flex flex-row w-[120px] min-w-[120px] h-full justify-center items-center",
                                             div { class: "text-[#35343f] font-semibold text-[14px]",
-                                                {survey.status.translate(&props.lang)}
+                                                {survey.survey_status().translate(&props.lang)}
                                             }
                                         }
 
@@ -196,6 +196,7 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                                 rsx! {
                                                     div {
                                                         class: "flex flex-row w-[120px] min-w-[120px] h-full justify-center items-center cursor-pointer",
+                                                        visibility: if survey.finished() { "hidden" } else { "" },
                                                         onclick: {
                                                             let id = survey.id.clone();
                                                             move |_| async move {
@@ -220,7 +221,9 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                             }
                                         }
 
-                                        div { class: "group relative",
+                                        div {
+                                            class: "group relative",
+                                            visibility: if survey.finished() { "hidden" } else { "" },
                                             div { class: "flex flex-row w-[90px] min-w-[90px] h-full justify-center items-center",
                                                 if survey.status == ProjectStatus::Ready {
                                                     button {
@@ -254,18 +257,21 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                                             }
                                                             li {
                                                                 class: "p-3 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer",
-                                                                onclick: move |_| {
-                                                                    let questions = survey.questions.clone();
-                                                                    let id = survey.id.clone();
-                                                                    async move {
-                                                                        ctrl.open_setting_reward_modal(
-                                                                                id,
-                                                                                props.lang,
-                                                                                survey.estimate_time,
-                                                                                survey.point,
-                                                                                questions.len() as i64,
-                                                                            )
-                                                                            .await;
+                                                                onclick: {
+                                                                    let survey = survey.clone();
+                                                                    move |_| {
+                                                                        let questions = survey.questions.clone();
+                                                                        let id = survey.id.clone();
+                                                                        async move {
+                                                                            ctrl.open_setting_reward_modal(
+                                                                                    id,
+                                                                                    props.lang,
+                                                                                    survey.estimate_time,
+                                                                                    survey.point,
+                                                                                    questions.len() as i64,
+                                                                                )
+                                                                                .await;
+                                                                        }
                                                                     }
                                                                 },
                                                                 "{translate.update_reward}"
diff --git a/packages/main-ui/src/utils/time.rs b/packages/main-ui/src/utils/time.rs
index 683f352d..7393ba1e 100644
--- a/packages/main-ui/src/utils/time.rs
+++ b/packages/main-ui/src/utils/time.rs
@@ -84,3 +84,9 @@ pub fn format_remaining_time(target_timestamp: i64) -> String {
 
     result.trim().to_string()
 }
+
+pub fn current_timestamp() -> i64 {
+    let now = Utc::now();
+    let timestamp_millis = now.timestamp();
+    timestamp_millis
+}
diff --git a/packages/models/src/deliberation_comments/deliberation_comment.rs b/packages/models/src/deliberation_comments/deliberation_comment.rs
index 9558d2d6..d6caf3b2 100644
--- a/packages/models/src/deliberation_comments/deliberation_comment.rs
+++ b/packages/models/src/deliberation_comments/deliberation_comment.rs
@@ -22,7 +22,7 @@ pub struct DeliberationComment {
     pub comment: String,
 
     // parent_id is used for reply to a comment
-    #[api_model(summary, many_to_one = deliberation_comments, query_action = replies_of)]
+    #[api_model(summary, query_action = replies_of)]
     pub parent_id: i64,
 
     // num_of_replies is used for the number of replies on a comment.
diff --git a/packages/models/src/survey/v2.rs b/packages/models/src/survey/v2.rs
index cfac97e7..12c7960b 100644
--- a/packages/models/src/survey/v2.rs
+++ b/packages/models/src/survey/v2.rs
@@ -92,6 +92,17 @@ impl SurveyV2 {
     }
 }
 
+#[derive(Translate, PartialEq, Default, Debug)]
+pub enum SurveyV2Status {
+    #[default]
+    #[translate(ko = "준비", en = "Ready")]
+    Ready,
+    #[translate(ko = "진행", en = "InProgress")]
+    InProgress,
+    #[translate(ko = "마감", en = "Finish")]
+    Finish,
+}
+
 #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
 #[cfg_attr(feature = "server", derive(schemars::JsonSchema, aide::OperationIo))]
 #[serde(rename_all = "snake_case", tag = "answer_type")]
@@ -288,6 +299,32 @@ pub struct ChoiceQuestion {
 }
 
 impl SurveyV2Summary {
+    pub fn finished(&self) -> bool {
+        let status = self.survey_status();
+
+        status == SurveyV2Status::Finish
+    }
+
+    pub fn survey_status(&self) -> SurveyV2Status {
+        let started_at = self.started_at;
+        let ended_at = self.ended_at;
+
+        let now = Utc::now();
+        let current = now.timestamp();
+
+        if started_at > 10000000000 {
+            return SurveyV2Status::default();
+        }
+
+        if started_at > current {
+            SurveyV2Status::Ready
+        } else if ended_at < current {
+            SurveyV2Status::Finish
+        } else {
+            SurveyV2Status::InProgress
+        }
+    }
+
     pub fn start_date(&self) -> String {
         let datetime = Utc.timestamp_opt(self.started_at, 0).unwrap();
         let formatted_date = datetime.format("%Y.%m.%d").to_string();


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/main-ui/src/pages/create/controller.rs (1)</summary><blockquote>

`319-320`: **Strong validation pattern for authentication number**

The implementation adds proper validation for the authentication number using regex pattern `^[a-zA-Z0-9+\-_.]{6}$`, which ensures exactly 6 characters of allowed characters. The error state handling is also well-structured.

One small concern: there's no validation feedback when a user is typing (only when they click the submit button). Consider adding real-time validation as users type, similar to how other fields might be validated.



You could implement real-time validation by modifying the `set_authentication_number` method:

```diff
pub fn set_authentication_number(&mut self, authentication_number: String) {
    self.authentication_number.set(authentication_number);
+   let re = Regex::new(r"^[a-zA-Z0-9+\-_.]{6}$").unwrap();
+   self.auth_error.set(!re.is_match(&self.get_authentication_number().as_str()) && !self.get_authentication_number().is_empty());
}
```


Also applies to: 325-325, 337-340, 354-361

</blockquote></details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/create_panel_modal.rs (4)</summary><blockquote>

`37-40`: **Spelling nitpick: consider renaming `selected_salarys`.**

Using `selected_salarys` might cause confusion. Renaming it to `selected_salaries` would improve clarity.

```diff
- let mut selected_salarys: Signal<Vec<String>> = use_signal(|| vec![]);
+ let mut selected_salaries: Signal<Vec<String>> = use_signal(|| vec![]);
```

---

`68-160`: **Modular approach for attributes is good; consider a more dynamic or unified pattern if these sets grow larger.**

You’ve nicely introduced separate signal states for each attribute type (ages, genders, regions, salaries). If more attributes are added in the future, centralizing them in a single data structure or dynamically rendering each set might reduce repetitive code. For now, this looks neat and understandable.

---

`172-196`: **Handle parse errors more gracefully.**

Using `unwrap_or_default()` will silently default invalid strings to an empty or None variant, potentially causing data inconsistencies. Consider logging these parse errors or displaying a user-facing warning to ensure the app handles invalid input responsibly.

```diff
- AgeV3::from_str(&age).unwrap_or_default()
+ match AgeV3::from_str(&age) {
+     Ok(value) => value,
+     Err(e) => {
+         // e.g., log::warn!("Invalid age: {}, error: {:?}", age, e);
+         // show user-facing error, or decide on fallback
+         AgeV3::None
+     }
+ }
```

---

`220-306`: **Select component looks good; consider a set-based lookup for optimization.**

The newly introduced `SelectedAttribute` component provides a clean, reusable approach. For very large attribute lists, you might switch from `.contains()` to a set-based approach to avoid repeated linear searches.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6f44548495c1b13d1b794d8723ab114415f73453 and e93f53e1c3bf1849a920f572bd72803328a02070.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `packages/api/src/utils/nonce_lab/create_survey_request.rs` (1 hunks)
* `packages/main-ui/src/pages/create/controller.rs` (5 hunks)
* `packages/main-ui/src/pages/create/i18n.rs` (1 hunks)
* `packages/main-ui/src/pages/create/page.rs` (1 hunks)
* `packages/main-ui/src/pages/create/step_three.rs` (2 hunks)
* `packages/main-ui/src/pages/surveys/components/create_panel_modal.rs` (4 hunks)
* `packages/main-ui/src/pages/surveys/components/survey.rs` (1 hunks)
* `packages/main-ui/src/pages/surveys/i18n.rs` (2 hunks)
* `packages/models/src/deliberation_comments/deliberation_comment.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (5)</summary>

<details>
<summary>packages/models/src/deliberation_comments/deliberation_comment.rs (1)</summary><blockquote>

<details>
<summary>packages/api/src/controllers/v2/deliberations/_id/comments.rs (1)</summary>

* `replies_of` (25-58)

</details>

</blockquote></details>
<details>
<summary>packages/api/src/utils/nonce_lab/create_survey_request.rs (2)</summary><blockquote>

<details>
<summary>packages/models/src/survey/v2.rs (3)</summary>

* `title` (152-159)
* `description` (161-168)
* `options` (233-239)

</details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/survey.rs (1)</summary>

* `Subjective` (207-242)

</details>

</blockquote></details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/survey.rs (1)</summary><blockquote>

<details>
<summary>packages/models/src/survey/v2.rs (2)</summary>

* `description` (161-168)
* `set_description` (170-185)

</details>

</blockquote></details>
<details>
<summary>packages/main-ui/src/pages/create/step_three.rs (1)</summary><blockquote>

<details>
<summary>packages/main-ui/src/pages/create/controller.rs (1)</summary>

* `get_authorization_error` (182-184)

</details>

</blockquote></details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/create_panel_modal.rs (1)</summary><blockquote>

<details>
<summary>packages/models/src/survey/response.rs (2)</summary>

* `translate` (229-268)
* `from_str` (185-225)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: main-ui
* GitHub Check: test
* GitHub Check: api
* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (16)</summary><blockquote>

<details>
<summary>packages/main-ui/src/pages/create/i18n.rs (1)</summary>

`207-210`: **Good addition for user feedback on authentication number validation.**

The new translation entry for `invalid_authorization_pattern` provides appropriate feedback for users when they enter an incorrect authentication number format. This enhances the user experience by giving clear guidance on input requirements.

</details>
<details>
<summary>packages/main-ui/src/pages/surveys/i18n.rs (2)</summary>

`94-97`: **Good addition for survey description input prompt.**

Adding a translation entry for `input_description` provides a clear prompt for users to enter descriptions in survey questions, enhancing the user experience with proper guidance in their preferred language.

---

`162-162`: **Minor improvement to age range representation.**

Changed the age range separator from tilde (~) to hyphen (-) in the Korean translation, which improves readability and maintains consistency with the English translation that already uses a hyphen.

</details>
<details>
<summary>packages/main-ui/src/pages/create/page.rs (1)</summary>

`141-141`: **Properly connected the new translation to the StepThreePage component.**

The new `invalid_authorization_pattern` property is now passed to the StepThreePage component, allowing it to display appropriate error messages when users enter incorrectly formatted authentication numbers.

</details>
<details>
<summary>packages/models/src/deliberation_comments/deliberation_comment.rs (1)</summary>

`25-25`: <details>
<summary>✅ Verification successful</summary>

**Simplified relationship annotation for parent_id field.**

Removed `many_to_one = deliberation_comments` from the annotation while retaining `summary` and `query_action = replies_of`. This simplification doesn't appear to affect functionality as the `replies_of` query action (shown in the relevant code snippet) still works correctly to fetch replies based on parent_id.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the relationship is defined elsewhere or if it's explicitly handled in the code

# Check for references to parent_id in relation to deliberation_comments
rg "parent_id.*deliberation_comments|deliberation_comments.*parent_id" --type rust

# Check implementation of the replies_of query action
rg "fn replies_of" --type rust -A 10
```

Length of output: 1406

---



</details>

**Annotation Simplification Verified**

The change simplifying the parent_id field annotation by removing the `many_to_one = deliberation_comments` parameter (so that only `summary` and `query_action = replies_of` remain) works as intended. The verification confirms that the `replies_of` query action (as implemented in the API controllers) correctly fetches replies via the parent_id field. Note that while other parts of the model still define relationships (e.g. using a `one_to_many` annotation elsewhere), this specific simplification has no negative impact on functionality.

</details>
<details>
<summary>packages/main-ui/src/pages/create/step_three.rs (2)</summary>

`31-31`: **Good addition of error message field**

The new `invalid_authorization_pattern` field in `StepThreeProps` will store the error message to display when authentication number validation fails, enhancing the user feedback system.

---

`90-96`: <details>
<summary>❓ Verification inconclusive</summary>

**Well-implemented error state UI feedback**

The conditional border color and error message implementation provides clear visual feedback to users when there's an authentication error. This follows the same pattern as other error states in the component, maintaining consistency in the user interface.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify that the error message string is passed from the parent component
fd "StepThreePage" --exec rg -A 5 "invalid_authorization_pattern:" {} \;
```

Length of output: 73

---



</details>

**Attention: Verify the "invalid_authorization_pattern" Prop Passing**

The error UI state implementation (conditional border style and error message) appears to follow the established pattern. However, our automatic search for the `invalid_authorization_pattern` property usage didn’t return any output. It’s unclear whether this prop is indeed being passed from the parent component.

**Action Required:**
- **Manual Verification:** Please confirm that the parent component passes down the `invalid_authorization_pattern` prop to this view (`packages/main-ui/src/pages/create/step_three.rs`). This manual check should ensure the error message content is properly received when an authorization error occurs.

</details>
<details>
<summary>packages/main-ui/src/pages/create/controller.rs (2)</summary>

`39-39`: **Good addition of auth error state**

Adding the `auth_error` signal provides a clean way to track and communicate authorization validation errors across the UI components.



Also applies to: 82-82

---

`182-184`: **Clear accessor method for auth error state**

The `get_authorization_error` method follows the established pattern in the codebase, providing a consistent interface to access the auth error state.

</details>
<details>
<summary>packages/api/src/utils/nonce_lab/create_survey_request.rs (2)</summary>

`205-228`: **Improved question field mapping for choice questions**

The changes correctly map the `description` field from `ChoiceQuestion` to the `question` field in `NonceLabSurveyQuestionType`. This provides better semantics and aligns with the UI changes that now allow users to enter descriptions for questions.

The use of `unwrap_or_default()` is prudent to ensure a default empty string when no description is provided.

---

`229-233`: **Consistent use of description field for text questions**

The implementation now correctly uses the `description` field instead of the `title` field for `ShortAnswer` and `Subjective` question types. This aligns with the UI changes and provides a more consistent handling of question data across different question types.



Also applies to: 235-239

</details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/survey.rs (1)</summary>

`130-145`: **Enhanced UI with description input field**

The addition of a description input field in the `Objective` component provides users with the ability to add more detailed explanations for objective-type questions. The implementation correctly handles state management and updates, with proper styling and placeholder text.

This change aligns well with the modifications in the API conversion code that now utilizes the description field for various question types.


The component correctly uses the `question.description()` and `question.set_description()` methods from the relevant code snippets, maintaining a clean separation of concerns between the UI and model layers.

</details>
<details>
<summary>packages/main-ui/src/pages/surveys/components/create_panel_modal.rs (4)</summary>

`1-2`: **Imports look good.**

These imports for string parsing are appropriate and necessary.

---

`6-9`: **No concerns with attribute imports.**

These newly introduced imports for GenderV2, RegionV2, SalaryV2, and AgeV3 look correct and align with the usage below.

---

`167-170`: **Straightforward retrieval of signals.**

Extracting and assigning the signal values is clear and maintains code readability.

---

`197-203`: **Verify correct handling of default user_count.**

You’re setting `user_count: 0` by default. Please ensure this aligns with the intended backend logic and doesn’t require updating when you send the panel creation request.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T10:22:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e93f53e1c3bf1849a920f572bd72803328a02070 and dcab2995778790a9e765be23767e5ffb9888f2bf.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/main-ui/src/pages/surveys/page.rs` (6 hunks)
* `packages/main-ui/src/utils/time.rs` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: api
* GitHub Check: main-ui
* GitHub Check: test
* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>packages/main-ui/src/pages/surveys/page.rs (4)</summary>

`24-33`: **Good implementation of SurveyStatus enum with internationalization support**

The SurveyStatus enum is well-designed with appropriate translation attributes for Korean and English. The default attribute and derived traits (Translate, PartialEq, Default, Debug) provide good functionality.

---

`203-210`: **Improved survey status display logic**

The conditional logic for displaying survey status is a good enhancement. It properly shows "Finish" status when the survey is determined to be finished based on timestamps, otherwise falls back to the original status.

---

`219-219`: **Good conditional visibility for UI elements**

Adding conditional visibility based on survey status prevents users from interacting with surveys that are already finished.

---

`244-246`: **Consistent UI approach for finished surveys**

The implementation consistently hides option controls for finished surveys, which prevents users from modifying surveys that have already ended.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T10:52:08Z
user: login: hackartists
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:02:19Z
user: login: hackartists
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:02:39Z
user: login: hackartists
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:05:29Z
user: login: hackartists
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:06:13Z
user: login: hackartists
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:06:32Z
user: login: hackartists
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:07:25Z

Review comments:
diff_hunk: @@ -361,3 +384,20 @@ pub fn PanelLabel(label: String, background_color: String) -> Element {
         }
     }
 }
+
+pub fn get_survey_status(started_at: i64, ended_at: i64) -> SurveyStatus {
+    let current = current_timestamp();
+
+    if started_at > 10000000000 {
+        tracing::error!("time parsing failed");
+        return SurveyStatus::default();
+    }
+
+    if started_at > current {
+        SurveyStatus::Ready
+    } else if ended_at < current {
+        SurveyStatus::Finish
+    } else {
+        SurveyStatus::InProgress
+    }
+}
path: packages/main-ui/src/pages/surveys/page.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Potential timestamp unit mismatch in get_survey_status function**

There appears to be an inconsistency in timestamp handling. The function checks if `started_at > 10000000000` to validate the timestamp, which suggests you're expecting millisecond timestamps. However, the `current_timestamp()` function returns seconds, not milliseconds.


Apply this fix to ensure consistent timestamp units:

```diff
pub fn get_survey_status(started_at: i64, ended_at: i64) -> SurveyStatus {
    let current = current_timestamp();

-   if started_at > 10000000000 {
+   // Check for invalid timestamp format (assuming timestamps in seconds)
+   if started_at > 253402300799 { // Max valid Unix timestamp in seconds (year 9999)
        tracing::error!("time parsing failed");
        return SurveyStatus::default();
    }

    if started_at > current {
        SurveyStatus::Ready
    } else if ended_at < current {
        SurveyStatus::Finish
    } else {
        SurveyStatus::InProgress
    }
}
```

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how timestamps are used in the codebase to determine if they're in seconds or milliseconds

# Check timestamp handling in other functions
rg -A 3 -B 3 "timestamp.*seconds|seconds.*timestamp" --type rust

# Check for uses of timestamp_millis vs timestamp
rg "timestamp_millis" --type rust
rg "timestamp\(\)" --type rust
```

Length of output: 4481

---



</details>

**Action Required: Adjust Timestamp Validation in `get_survey_status`**

The investigation confirms that the `current_timestamp()` function returns Unix timestamps in seconds throughout the codebase. As the survey page operates on seconds, the current check of  
```rust
if started_at > 10000000000 {
```
may be misinterpreting timestamps that are accidentally provided in milliseconds. To more accurately validate seconds-based timestamps (considering valid Unix timestamps up to the year 9999), update the condition as follows:

```diff
pub fn get_survey_status(started_at: i64, ended_at: i64) -> SurveyStatus {
    let current = current_timestamp();

-   if started_at > 10000000000 {
+   // Check for invalid timestamp format (assuming timestamps in seconds)
+   if started_at > 253402300799 { // Max valid Unix timestamp in seconds (year 9999)
        tracing::error!("time parsing failed");
        return SurveyStatus::default();
    }

    if started_at > current {
        SurveyStatus::Ready
    } else if ended_at < current {
        SurveyStatus::Finish
    } else {
        SurveyStatus::InProgress
    }
}
```

This change will ensure that if an unexpectedly high timestamp (likely in milliseconds) is provided, it’s caught correctly while maintaining consistency with the seconds-based timestamps used elsewhere.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-24T10:52:08Z
author_association: NONE
diff_hunk: @@ -84,3 +84,9 @@ pub fn format_remaining_time(target_timestamp: i64) -> String {
 
     result.trim().to_string()
 }
+
+pub fn current_timestamp() -> i64 {
+    let now = Utc::now();
+    let timestamp_millis = now.timestamp();
+    timestamp_millis
+}
path: packages/main-ui/src/utils/time.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Unit inconsistency in current_timestamp implementation**

The `current_timestamp()` function returns seconds, not milliseconds, as indicated by `now.timestamp()`. However, other functions in this file, like `convert_timestamp_to_fmt_string`, use `timestamp_millis_opt`, suggesting that timestamps are expected to be in milliseconds throughout the codebase.


This could lead to incompatible timestamp comparisons. Modify the function to return milliseconds for consistency:

```diff
pub fn current_timestamp() -> i64 {
    let now = Utc::now();
-   let timestamp_millis = now.timestamp();
+   let timestamp_millis = now.timestamp_millis();
    timestamp_millis
}
```

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how timestamps are used in this codebase

# Look for all timestamp_millis uses
echo "Checking for timestamp_millis usage:"
rg "timestamp_millis" --type rust

# Look for timestamp() uses
echo -e "\nChecking for timestamp() usage:"
rg "timestamp\(\)" --type rust

# Check survey status-related code to understand timestamp usage context
echo -e "\nChecking survey status related code:"
rg -A 5 -B 5 "SurveyStatus" --type rust
```

Length of output: 43288

---



</details>

**Action Required: Update `current_timestamp()` to Return Milliseconds**

After verifying the usage of timestamp functions across the codebase, it's clear that timestamps are expected to be in milliseconds. In particular, while other parts of the system (for example, in `packages/models/src/survey/mod.rs` and elsewhere) consistently use `timestamp_millis`, the function in `packages/main-ui/src/utils/time.rs` calls `now.timestamp()`, which returns seconds. This discrepancy can lead to incorrect comparisons and logic issues when timestamps are used.

**Recommended Fix:**

- **Location:** `packages/main-ui/src/utils/time.rs` (Lines 88–92)  
- **Change:** Replace the call to `now.timestamp()` with `now.timestamp_millis()` to ensure the function returns milliseconds.

```diff
pub fn current_timestamp() -> i64 {
    let now = Utc::now();
-   let timestamp_millis = now.timestamp();
+   let timestamp_millis = now.timestamp_millis();
    timestamp_millis
}
```

Please update the implementation accordingly.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
pub fn current_timestamp() -> i64 {
    let now = Utc::now();
    let timestamp_millis = now.timestamp_millis();
    timestamp_millis
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-24T10:52:08Z
author_association: NONE
diff_hunk: @@ -86,6 +87,13 @@ pub fn StepThreePage(props: StepThreeProps) -> Element {
                                     onchange: move |e| {
                                         ctrl.set_authentication_number(e);
                                     },
+                                    border: if ctrl.get_authorization_error() { "border-[#ff0000]" } else { "border-[#E0E0E0]" },
path: packages/main-ui/src/pages/create/step_three.rs
user: login: hackartists
body: input.css 에 정의하고 써주세요.
created_at: 2025-03-24T21:02:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,6 +87,13 @@ pub fn StepThreePage(props: StepThreeProps) -> Element {
                                     onchange: move |e| {
                                         ctrl.set_authentication_number(e);
                                     },
+                                    border: if ctrl.get_authorization_error() { "border-[#ff0000]" } else { "border-[#E0E0E0]" },
+                                }
+
+                                if ctrl.get_authorization_error() {
+                                    div { class: "mt-[10px] font-normal text-[#ff0000] text-[12px]",
path: packages/main-ui/src/pages/create/step_three.rs
user: login: hackartists
body: 이 프로젝트가 지금 tailwindcss 3 인가요? 그러면 업데이트 해주세요.
created_at: 2025-03-24T21:02:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,7 +200,14 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                         }
                                         div { class: "flex flex-row w-[120px] min-w-[120px] h-full justify-center items-center",
                                             div { class: "text-[#35343f] font-semibold text-[14px]",
-                                                {survey.status.translate(&props.lang)}
+                                                {
+                                                    if get_survey_status(survey.started_at, survey.ended_at) == SurveyStatus::Finish
+                                                    {
+                                                        SurveyStatus::Finish.translate(&props.lang)
+                                                    } else {
+                                                        survey.status.translate(&props.lang)
+                                                    }
+                                                }
path: packages/main-ui/src/pages/surveys/page.rs
user: login: hackartists
body: ```suggestion
 {survey.status().translate(&props.lan)}
```
이렇게 되도록 전체 코드를 수정하는게 좋습니다. status 함수 안에서 started_at 등을 다 체크하고 상태를 내보내주면 UI 에 이렇게 로직이 들어갈 필요는 없어요.
created_at: 2025-03-24T21:05:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,6 +216,7 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                                 rsx! {
                                                     div {
                                                         class: "flex flex-row w-[120px] min-w-[120px] h-full justify-center items-center cursor-pointer",
+                                                        visibility: if get_survey_status(survey.started_at, survey.ended_at) == SurveyStatus::Finish { "hidden" } else { "" },
path: packages/main-ui/src/pages/surveys/page.rs
user: login: hackartists
body: ```suggestion
                                                        visibility: if survey.finished() { "hidden" } else { "" },
```
created_at: 2025-03-24T21:06:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,7 +241,9 @@ pub fn SurveyPage(props: SurveyProps) -> Element {
                                             }
                                         }
 
-                                        div { class: "group relative",
+                                        div {
+                                            class: "group relative",
+                                            visibility: if get_survey_status(survey.started_at, survey.ended_at) == SurveyStatus::Finish { "hidden" } else { "" },
path: packages/main-ui/src/pages/surveys/page.rs
user: login: hackartists
body: 동일
created_at: 2025-03-24T21:06:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-24T10:17:58Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
The pull request includes several modifications across multiple packages. The API conversion for survey questions now leverages the description field for various question types. Changes in the main UI enhance authorization error handling by adding a new signal, validation via regex, and corresponding UI feedback. Additionally, survey components are refactored to simplify attribute state management and improve question description editing. A minor annotation update has been applied to the models to simplify the metadata for the parent field.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `packages/api/.../create_survey_request.rs` | Update conversion logic in `From<Question>` to use the `description` field for SingleChoice, MultipleChoice, ShortAnswer, and Subjective questions. |
| `packages/main-ui/src/pages/create/controller.rs` | Introduce new signal `auth_error` with corresponding getter and regex validation for `authentication_number`. |
| `packages/main-ui/src/pages/create/i18n.rs` | Add new translation entry `invalid_authorization_pattern` for error feedback. |
| `packages/main-ui/src/pages/create/page.rs`, `.../step_three.rs` | Update UI components to render error states for authorization and add property for `invalid_authorization_pattern`. |
| `packages/main-ui/src/pages/surveys/components/create_panel_modal.rs` | Refactor survey components: replace consolidated attribute signal with separate ones and add a new `SelectedAttribute` component. |
| `packages/main-ui/src/pages/surveys/components/survey.rs` | Update event handling for question updates by adding a description input field and modifying the `onchange` event handler. |
| `packages/main-ui/src/pages/surveys/i18n.rs` | Add new translation entry `input_description` and update the Korean translation for the `twenty` entry. |
| `packages/models/.../deliberation_comment.rs` | Modify the `parent_id` field annotation by removing the `many_to_one` attribute from the API model metadata. |
| `packages/main-ui/src/pages/surveys/page.rs` | Introduce new enum `SurveyV2Status` for survey status management and add functions `finished` and `survey_status`. |
| `packages/main-ui/src/utils/time.rs` | Add new public function `current_timestamp` to retrieve the current UTC timestamp. |
| `packages/api/Cargo.toml` | Update package version from "0.3.23" to "0.3.24". |
| `packages/main-ui/Cargo.toml` | Update package version from "0.3.19" to "0.3.20". |
| `.github/workflows/pr-workflow.yml`, `.github/workflows/prod-workflow.yaml`, `packages/main-ui/Makefile` | Modify Tailwind CSS installation commands to use `@tailwindcss/cli` instead of a specific version. |
| `packages/main-ui/input.css` | Remove the old `input.css` file containing Tailwind CSS directives. |
| `packages/main-ui/public/input.css` | Introduce a new `input.css` file with Tailwind CSS directives and a custom theme. |
| `packages/main-ui/src/main.rs` | Simplify stylesheet loading process by embedding CSS directly and removing the `load_tailwindcss` function. |
| `packages/models/src/survey/v2.rs` | Add new enum `SurveyV2Status` and methods `finished` and `survey_status` for survey status management. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Controller
    participant RegexValidator

    User->>Controller: Click join membership button
    Controller->>RegexValidator: Validate authentication_number
    alt Invalid Format
        RegexValidator-->>Controller: Invalid
        Controller->>User: Set auth_error to true and display error feedback
    else Valid Format
        RegexValidator-->>Controller: Valid
        Controller->>User: Reset auth_error and proceed
    end
```

```mermaid
sequenceDiagram
    participant User
    participant ObjectiveComponent
    participant ParentComponent

    User->>ObjectiveComponent: Enters question description
    ObjectiveComponent->>ObjectiveComponent: Update local description state
    ObjectiveComponent->>ParentComponent: Trigger onchange event with updated question data
    ParentComponent-->>ObjectiveComponent: Update question list and re-render view
```

## Possibly related PRs
- biyard/voice-korea#131: The changes in the main PR, which enhance the handling of the `description` field in survey questions, are related to the modifications in the retrieved PR that also focus on improving the survey functionality, including validation for required fields like title and description.
- biyard/voice-korea#219: The changes in the main PR are directly related to the same implementation of the `From<Question> for NonceLabSurveyQuestion` conversion logic as seen in the retrieved PR, which also modifies the handling of the `description` field for various question types.
- biyard/voice-korea#36: The changes in the main PR, which enhance the handling of the `description` field in survey questions, are related to the retrieved PR as both involve modifications to survey-related structures and functionalities, specifically in how surveys are created and managed.

## Poem
> Oh merry hops in code we play,  
> A rabbit's cheer for changes today!  
> Descriptions now dance with every choice,  
> Validation sings with a precise voice.  
> Surveys and panels, light and clear,  
> Our code garden blooms—let's all cheer!  
> 🐰💻 Hop on to the new frontier!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7229c16a97953ab5dcc1d60171b0d91f3d0c7413 and 1c04588fe1de7c51fcd7db254b498897748c5589.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `.github/workflows/pr-workflow.yml` (1 hunks)
* `.github/workflows/prod-workflow.yaml` (1 hunks)
* `packages/main-ui/Makefile` (2 hunks)
* `packages/main-ui/input.css` (0 hunks)
* `packages/main-ui/public/input.css` (1 hunks)
* `packages/main-ui/src/main.rs` (2 hunks)
* `packages/main-ui/src/pages/create/step_three.rs` (2 hunks)
* `packages/main-ui/src/pages/surveys/page.rs` (5 hunks)
* `packages/models/src/survey/v2.rs` (2 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=biyard/voice-korea&utm_content=219):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: victor138128
created_at: 2025-03-25T01:25:08Z
body: @hackartists 주신 코멘트 모두 반영완료했습니다.
author_association: CONTRIBUTOR