diff --git a/.gitignore b/.gitignore
index b1e775f..50905b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,6 @@ secrets/
tmp/
temp/
*.tmp
+
+# Cursor (local AI assistant rules and cache)
+.cursor/
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxF1_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxF1_curve.png
new file mode 100644
index 0000000..f8fcbb0
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxF1_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxPR_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxPR_curve.png
new file mode 100644
index 0000000..f02a170
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxPR_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxP_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxP_curve.png
new file mode 100644
index 0000000..9c4d781
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxP_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxR_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxR_curve.png
new file mode 100644
index 0000000..d03445c
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/BoxR_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix.png
new file mode 100644
index 0000000..60f86b0
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png
new file mode 100644
index 0000000..5cfa788
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/labels.jpg
new file mode 100644
index 0000000..41e83b2
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/results.png b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/results.png
new file mode 100644
index 0000000..b964b97
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/results.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch0.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch0.jpg
new file mode 100644
index 0000000..6c48041
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch0.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch1.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch1.jpg
new file mode 100644
index 0000000..3feaddd
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch1.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch2.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch2.jpg
new file mode 100644
index 0000000..1ebf79f
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/train_batch2.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_labels.jpg
new file mode 100644
index 0000000..f9f0710
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_pred.jpg
new file mode 100644
index 0000000..97c5117
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch0_pred.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_labels.jpg
new file mode 100644
index 0000000..d8dc9c2
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_pred.jpg
new file mode 100644
index 0000000..3286033
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch1_pred.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_labels.jpg
new file mode 100644
index 0000000..02a4c6c
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_pred.jpg
new file mode 100644
index 0000000..4b7c47a
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8n_imgsz_1280/train/val_batch2_pred.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxF1_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxF1_curve.png
new file mode 100644
index 0000000..073106a
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxF1_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxPR_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxPR_curve.png
new file mode 100644
index 0000000..157f8c5
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxPR_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxP_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxP_curve.png
new file mode 100644
index 0000000..c320d32
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxP_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxR_curve.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxR_curve.png
new file mode 100644
index 0000000..c94c969
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/BoxR_curve.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix.png
new file mode 100644
index 0000000..acd1247
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png
new file mode 100644
index 0000000..c353941
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/labels.jpg
new file mode 100644
index 0000000..9fba26d
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/results.png b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/results.png
new file mode 100644
index 0000000..d6384b0
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/results.png differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch0.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch0.jpg
new file mode 100644
index 0000000..68df35a
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch0.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch1.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch1.jpg
new file mode 100644
index 0000000..2bf07fa
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch1.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch2.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch2.jpg
new file mode 100644
index 0000000..dc846d6
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch2.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7560.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7560.jpg
new file mode 100644
index 0000000..c714ce7
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7560.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7561.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7561.jpg
new file mode 100644
index 0000000..558bf50
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7561.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7562.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7562.jpg
new file mode 100644
index 0000000..70537de
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/train_batch7562.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_labels.jpg
new file mode 100644
index 0000000..2e3b499
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_pred.jpg
new file mode 100644
index 0000000..f3ba50d
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch0_pred.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_labels.jpg
new file mode 100644
index 0000000..3ca3d88
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_pred.jpg
new file mode 100644
index 0000000..50c66fd
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch1_pred.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_labels.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_labels.jpg
new file mode 100644
index 0000000..f58121c
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_labels.jpg differ
diff --git a/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_pred.jpg b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_pred.jpg
new file mode 100644
index 0000000..5dc5204
Binary files /dev/null and b/ai-service/models/Detection/Model_routing_1004/yolov8s_imgsz_2048/train/val_batch2_pred.jpg differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 79_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 79_routed.jpg"
new file mode 100644
index 0000000..300f2a5
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 79_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 80_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 80_routed.jpg"
new file mode 100644
index 0000000..e4c947f
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 80_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 81_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 81_routed.jpg"
new file mode 100644
index 0000000..5e519f1
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 81_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 82_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 82_routed.jpg"
new file mode 100644
index 0000000..eee5cff
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 82_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 83_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 83_routed.jpg"
new file mode 100644
index 0000000..20bdfd6
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 83_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 84_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 84_routed.jpg"
new file mode 100644
index 0000000..957875f
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 84_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 85_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 85_routed.jpg"
new file mode 100644
index 0000000..1662751
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\213\244\354\240\204 \353\252\250\354\235\230\352\263\240\354\202\254) - 85_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 10_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 10_routed.jpg"
new file mode 100644
index 0000000..5472784
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 10_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 11_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 11_routed.jpg"
new file mode 100644
index 0000000..01eeced
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 11_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 1_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 1_routed.jpg"
new file mode 100644
index 0000000..a68f407
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 1_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 2_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 2_routed.jpg"
new file mode 100644
index 0000000..0f4ab75
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 2_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 3_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 3_routed.jpg"
new file mode 100644
index 0000000..da6672c
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 3_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 4_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 4_routed.jpg"
new file mode 100644
index 0000000..d44db44
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 4_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 5_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 5_routed.jpg"
new file mode 100644
index 0000000..8e189da
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 5_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 6_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 6_routed.jpg"
new file mode 100644
index 0000000..0926701
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 6_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 7_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 7_routed.jpg"
new file mode 100644
index 0000000..869dc61
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 7_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 8_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 8_routed.jpg"
new file mode 100644
index 0000000..6dc069b
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 8_routed.jpg" differ
diff --git "a/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 9_routed.jpg" "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 9_routed.jpg"
new file mode 100644
index 0000000..c793b70
Binary files /dev/null and "b/ai-service/models/Detection/experiment_routing_0930/inference_test_routing_0930/visualizations/\355\225\231\354\203\235\354\235\264 \355\221\274 \353\254\270\354\240\234(\354\234\240\355\230\225\355\216\270) - 9_routed.jpg" differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxF1_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxF1_curve.png
new file mode 100644
index 0000000..c69d3ad
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxF1_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxPR_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxPR_curve.png
new file mode 100644
index 0000000..fc8ce57
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxPR_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxP_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxP_curve.png
new file mode 100644
index 0000000..8df4fa0
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxP_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxR_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxR_curve.png
new file mode 100644
index 0000000..0d48e25
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/BoxR_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix.png
new file mode 100644
index 0000000..d84c361
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png
new file mode 100644
index 0000000..6eea7cb
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/confusion_matrix_normalized.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/labels.jpg
new file mode 100644
index 0000000..d190e9d
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/results.png b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/results.png
new file mode 100644
index 0000000..6ee7ff1
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/results.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch0.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch0.jpg
new file mode 100644
index 0000000..04b810c
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch0.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch1.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch1.jpg
new file mode 100644
index 0000000..4666128
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch1.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2.jpg
new file mode 100644
index 0000000..eebc4fc
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2070.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2070.jpg
new file mode 100644
index 0000000..1328172
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2070.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2071.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2071.jpg
new file mode 100644
index 0000000..b85daa2
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2071.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2072.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2072.jpg
new file mode 100644
index 0000000..9c8ecc9
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/train_batch2072.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_labels.jpg
new file mode 100644
index 0000000..027ff13
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_pred.jpg
new file mode 100644
index 0000000..c9ad87b
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch0_pred.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_labels.jpg
new file mode 100644
index 0000000..1a2f3b3
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_pred.jpg
new file mode 100644
index 0000000..6634c8f
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch1_pred.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_labels.jpg
new file mode 100644
index 0000000..66f2c2a
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_pred.jpg
new file mode 100644
index 0000000..ae65459
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8n_imgsz_1280/train/val_batch2_pred.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxF1_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxF1_curve.png
new file mode 100644
index 0000000..f6400d2
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxF1_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxPR_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxPR_curve.png
new file mode 100644
index 0000000..4342a17
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxPR_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxP_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxP_curve.png
new file mode 100644
index 0000000..6fc5a31
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxP_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxR_curve.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxR_curve.png
new file mode 100644
index 0000000..68b5e2f
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/BoxR_curve.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix.png
new file mode 100644
index 0000000..c064374
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png
new file mode 100644
index 0000000..2a92763
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/confusion_matrix_normalized.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/labels.jpg
new file mode 100644
index 0000000..f717587
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/results.png b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/results.png
new file mode 100644
index 0000000..4eb5a8d
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/results.png differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch0.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch0.jpg
new file mode 100644
index 0000000..eb8cd7e
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch0.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch1.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch1.jpg
new file mode 100644
index 0000000..efd8d2f
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch1.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch2.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch2.jpg
new file mode 100644
index 0000000..000a47d
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch2.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4050.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4050.jpg
new file mode 100644
index 0000000..167b078
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4050.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4051.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4051.jpg
new file mode 100644
index 0000000..abe63cf
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4051.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4052.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4052.jpg
new file mode 100644
index 0000000..abffc0d
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/train_batch4052.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_labels.jpg
new file mode 100644
index 0000000..b5e32bc
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_pred.jpg
new file mode 100644
index 0000000..587fe08
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch0_pred.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_labels.jpg
new file mode 100644
index 0000000..6a6fc52
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_pred.jpg
new file mode 100644
index 0000000..9d9f490
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch1_pred.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_labels.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_labels.jpg
new file mode 100644
index 0000000..ba46a3b
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_labels.jpg differ
diff --git a/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_pred.jpg b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_pred.jpg
new file mode 100644
index 0000000..5b35c10
Binary files /dev/null and b/ai-service/models/Detection/experiment_routing_0930/yolov8s_imgsz_2048/train/val_batch2_pred.jpg differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_00_conf0.50.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_00_conf0.50.jpg"
new file mode 100644
index 0000000..952961d
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_00_conf0.50.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_01_conf0.34.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_01_conf0.34.jpg"
new file mode 100644
index 0000000..117720d
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_01_conf0.34.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_02_conf0.31.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_02_conf0.31.jpg"
new file mode 100644
index 0000000..8b1145b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 10_section_02_conf0.31.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_00_conf0.55.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_00_conf0.55.jpg"
new file mode 100644
index 0000000..893962c
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_00_conf0.55.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_01_conf0.52.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_01_conf0.52.jpg"
new file mode 100644
index 0000000..e009e8b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_01_conf0.52.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_02_conf0.51.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_02_conf0.51.jpg"
new file mode 100644
index 0000000..64b9033
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_02_conf0.51.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_03_conf0.50.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_03_conf0.50.jpg"
new file mode 100644
index 0000000..0cc807a
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_03_conf0.50.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_04_conf0.45.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_04_conf0.45.jpg"
new file mode 100644
index 0000000..0fdcc2a
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_04_conf0.45.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_05_conf0.36.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_05_conf0.36.jpg"
new file mode 100644
index 0000000..f0c5427
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_05_conf0.36.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_06_conf0.31.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_06_conf0.31.jpg"
new file mode 100644
index 0000000..8760733
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 11_section_06_conf0.31.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_00_conf0.75.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_00_conf0.75.jpg"
new file mode 100644
index 0000000..2d173df
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_00_conf0.75.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_01_conf0.71.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_01_conf0.71.jpg"
new file mode 100644
index 0000000..09062f7
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_01_conf0.71.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_02_conf0.62.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_02_conf0.62.jpg"
new file mode 100644
index 0000000..bd13228
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_02_conf0.62.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_03_conf0.55.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_03_conf0.55.jpg"
new file mode 100644
index 0000000..22abf14
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_03_conf0.55.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_04_conf0.52.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_04_conf0.52.jpg"
new file mode 100644
index 0000000..ec708f7
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_04_conf0.52.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_05_conf0.49.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_05_conf0.49.jpg"
new file mode 100644
index 0000000..8e27a93
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_05_conf0.49.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_06_conf0.45.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_06_conf0.45.jpg"
new file mode 100644
index 0000000..3bf5b1d
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 12_section_06_conf0.45.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_00_conf0.73.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_00_conf0.73.jpg"
new file mode 100644
index 0000000..a35e53a
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_00_conf0.73.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_01_conf0.72.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_01_conf0.72.jpg"
new file mode 100644
index 0000000..b07a9af
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_01_conf0.72.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_02_conf0.72.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_02_conf0.72.jpg"
new file mode 100644
index 0000000..a6d7960
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_02_conf0.72.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_03_conf0.68.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_03_conf0.68.jpg"
new file mode 100644
index 0000000..4c2d14a
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_03_conf0.68.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_04_conf0.67.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_04_conf0.67.jpg"
new file mode 100644
index 0000000..89b01c6
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_04_conf0.67.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_05_conf0.60.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_05_conf0.60.jpg"
new file mode 100644
index 0000000..a75a2aa
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_05_conf0.60.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_06_conf0.45.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_06_conf0.45.jpg"
new file mode 100644
index 0000000..6ef4fa8
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_06_conf0.45.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_07_conf0.42.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_07_conf0.42.jpg"
new file mode 100644
index 0000000..2b1f148
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_07_conf0.42.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_08_conf0.31.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_08_conf0.31.jpg"
new file mode 100644
index 0000000..45c2f54
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 13_section_08_conf0.31.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_00_conf0.73.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_00_conf0.73.jpg"
new file mode 100644
index 0000000..ba02a07
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_00_conf0.73.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_01_conf0.66.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_01_conf0.66.jpg"
new file mode 100644
index 0000000..4224fb6
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_01_conf0.66.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_02_conf0.63.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_02_conf0.63.jpg"
new file mode 100644
index 0000000..5aa2993
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_02_conf0.63.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_03_conf0.55.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_03_conf0.55.jpg"
new file mode 100644
index 0000000..a447337
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_03_conf0.55.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_04_conf0.51.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_04_conf0.51.jpg"
new file mode 100644
index 0000000..4439389
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_04_conf0.51.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_05_conf0.50.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_05_conf0.50.jpg"
new file mode 100644
index 0000000..c6e1355
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_05_conf0.50.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_06_conf0.35.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_06_conf0.35.jpg"
new file mode 100644
index 0000000..a95d192
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 14_section_06_conf0.35.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_00_conf0.75.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_00_conf0.75.jpg"
new file mode 100644
index 0000000..083d2db
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_00_conf0.75.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_01_conf0.72.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_01_conf0.72.jpg"
new file mode 100644
index 0000000..fbac37b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_01_conf0.72.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_02_conf0.69.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_02_conf0.69.jpg"
new file mode 100644
index 0000000..12c75b9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_02_conf0.69.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_03_conf0.67.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_03_conf0.67.jpg"
new file mode 100644
index 0000000..e5c3ab5
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_03_conf0.67.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_04_conf0.66.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_04_conf0.66.jpg"
new file mode 100644
index 0000000..0e02293
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_04_conf0.66.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_05_conf0.58.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_05_conf0.58.jpg"
new file mode 100644
index 0000000..e147335
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_05_conf0.58.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_06_conf0.52.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_06_conf0.52.jpg"
new file mode 100644
index 0000000..f0d9267
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_06_conf0.52.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_07_conf0.28.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_07_conf0.28.jpg"
new file mode 100644
index 0000000..0c3c91b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 15_section_07_conf0.28.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_00_conf0.64.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_00_conf0.64.jpg"
new file mode 100644
index 0000000..4895289
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_00_conf0.64.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_01_conf0.62.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_01_conf0.62.jpg"
new file mode 100644
index 0000000..01a3508
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_01_conf0.62.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_02_conf0.50.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_02_conf0.50.jpg"
new file mode 100644
index 0000000..8ba3f61
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_02_conf0.50.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_03_conf0.43.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_03_conf0.43.jpg"
new file mode 100644
index 0000000..603ef97
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 16_section_03_conf0.43.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_00_conf0.61.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_00_conf0.61.jpg"
new file mode 100644
index 0000000..cee5b35
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_00_conf0.61.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_01_conf0.55.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_01_conf0.55.jpg"
new file mode 100644
index 0000000..3f2bf71
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_01_conf0.55.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_02_conf0.51.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_02_conf0.51.jpg"
new file mode 100644
index 0000000..12fbe0f
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_02_conf0.51.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_03_conf0.46.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_03_conf0.46.jpg"
new file mode 100644
index 0000000..c1c5de8
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_03_conf0.46.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_04_conf0.44.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_04_conf0.44.jpg"
new file mode 100644
index 0000000..cff0e5c
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 1_section_04_conf0.44.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_00_conf0.71.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_00_conf0.71.jpg"
new file mode 100644
index 0000000..bb3ef15
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_00_conf0.71.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_01_conf0.64.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_01_conf0.64.jpg"
new file mode 100644
index 0000000..89723ea
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_01_conf0.64.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_02_conf0.62.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_02_conf0.62.jpg"
new file mode 100644
index 0000000..9aae8b9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_02_conf0.62.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_03_conf0.44.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_03_conf0.44.jpg"
new file mode 100644
index 0000000..78d16b8
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 2_section_03_conf0.44.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_00_conf0.71.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_00_conf0.71.jpg"
new file mode 100644
index 0000000..c1beeaf
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_00_conf0.71.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_01_conf0.63.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_01_conf0.63.jpg"
new file mode 100644
index 0000000..66ad771
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_01_conf0.63.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_02_conf0.52.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_02_conf0.52.jpg"
new file mode 100644
index 0000000..324778b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_02_conf0.52.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_03_conf0.50.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_03_conf0.50.jpg"
new file mode 100644
index 0000000..ecb0a25
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_03_conf0.50.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_04_conf0.47.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_04_conf0.47.jpg"
new file mode 100644
index 0000000..e7b0338
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_04_conf0.47.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_05_conf0.32.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_05_conf0.32.jpg"
new file mode 100644
index 0000000..f051ed9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_05_conf0.32.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_06_conf0.30.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_06_conf0.30.jpg"
new file mode 100644
index 0000000..8548567
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_06_conf0.30.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_07_conf0.28.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_07_conf0.28.jpg"
new file mode 100644
index 0000000..86f6b87
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 3_section_07_conf0.28.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_00_conf0.79.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_00_conf0.79.jpg"
new file mode 100644
index 0000000..219a514
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_00_conf0.79.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_01_conf0.62.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_01_conf0.62.jpg"
new file mode 100644
index 0000000..89a53be
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_01_conf0.62.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_02_conf0.61.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_02_conf0.61.jpg"
new file mode 100644
index 0000000..ae0237c
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_02_conf0.61.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_03_conf0.57.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_03_conf0.57.jpg"
new file mode 100644
index 0000000..df6a13d
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_03_conf0.57.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_04_conf0.44.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_04_conf0.44.jpg"
new file mode 100644
index 0000000..fc44ea7
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_04_conf0.44.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_05_conf0.38.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_05_conf0.38.jpg"
new file mode 100644
index 0000000..8f2484a
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 4_section_05_conf0.38.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_00_conf0.70.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_00_conf0.70.jpg"
new file mode 100644
index 0000000..016acf6
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_00_conf0.70.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_01_conf0.70.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_01_conf0.70.jpg"
new file mode 100644
index 0000000..1af845c
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_01_conf0.70.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_02_conf0.66.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_02_conf0.66.jpg"
new file mode 100644
index 0000000..f56d0c9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_02_conf0.66.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_03_conf0.60.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_03_conf0.60.jpg"
new file mode 100644
index 0000000..b7f4822
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_03_conf0.60.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_04_conf0.59.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_04_conf0.59.jpg"
new file mode 100644
index 0000000..3b361a5
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_04_conf0.59.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_06_conf0.36.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_06_conf0.36.jpg"
new file mode 100644
index 0000000..8fdf829
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_06_conf0.36.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_07_conf0.29.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_07_conf0.29.jpg"
new file mode 100644
index 0000000..68422b2
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 5_section_07_conf0.29.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_00_conf0.72.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_00_conf0.72.jpg"
new file mode 100644
index 0000000..5743a78
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_00_conf0.72.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_01_conf0.63.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_01_conf0.63.jpg"
new file mode 100644
index 0000000..054c683
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_01_conf0.63.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_02_conf0.48.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_02_conf0.48.jpg"
new file mode 100644
index 0000000..c8297be
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_02_conf0.48.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_03_conf0.47.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_03_conf0.47.jpg"
new file mode 100644
index 0000000..14d8f0e
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_03_conf0.47.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_04_conf0.27.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_04_conf0.27.jpg"
new file mode 100644
index 0000000..b5b2d13
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 6_section_04_conf0.27.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_00_conf0.60.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_00_conf0.60.jpg"
new file mode 100644
index 0000000..c6bbb73
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_00_conf0.60.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_01_conf0.60.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_01_conf0.60.jpg"
new file mode 100644
index 0000000..1b0eb54
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_01_conf0.60.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_02_conf0.54.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_02_conf0.54.jpg"
new file mode 100644
index 0000000..4aaebf9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_02_conf0.54.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_03_conf0.48.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_03_conf0.48.jpg"
new file mode 100644
index 0000000..95b76ad
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_03_conf0.48.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_04_conf0.43.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_04_conf0.43.jpg"
new file mode 100644
index 0000000..a6d3cdd
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_04_conf0.43.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_05_conf0.43.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_05_conf0.43.jpg"
new file mode 100644
index 0000000..9ed2d27
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_05_conf0.43.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_06_conf0.36.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_06_conf0.36.jpg"
new file mode 100644
index 0000000..fe123e9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 7_section_06_conf0.36.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_00_conf0.78.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_00_conf0.78.jpg"
new file mode 100644
index 0000000..14e55bd
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_00_conf0.78.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_01_conf0.71.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_01_conf0.71.jpg"
new file mode 100644
index 0000000..026c1ea
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_01_conf0.71.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_02_conf0.68.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_02_conf0.68.jpg"
new file mode 100644
index 0000000..5cada42
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_02_conf0.68.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_03_conf0.61.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_03_conf0.61.jpg"
new file mode 100644
index 0000000..a3bbf25
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_03_conf0.61.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_04_conf0.48.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_04_conf0.48.jpg"
new file mode 100644
index 0000000..34b354e
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_04_conf0.48.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_05_conf0.44.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_05_conf0.44.jpg"
new file mode 100644
index 0000000..c8501d9
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_05_conf0.44.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_06_conf0.43.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_06_conf0.43.jpg"
new file mode 100644
index 0000000..dfa41e2
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_06_conf0.43.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_07_conf0.29.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_07_conf0.29.jpg"
new file mode 100644
index 0000000..c10571d
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 8_section_07_conf0.29.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_00_conf0.54.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_00_conf0.54.jpg"
new file mode 100644
index 0000000..cc2ee8b
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_00_conf0.54.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_01_conf0.52.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_01_conf0.52.jpg"
new file mode 100644
index 0000000..40e1758
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_01_conf0.52.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_02_conf0.45.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_02_conf0.45.jpg"
new file mode 100644
index 0000000..512b61c
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_02_conf0.45.jpg" differ
diff --git "a/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_03_conf0.44.jpg" "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_03_conf0.44.jpg"
new file mode 100644
index 0000000..637dc64
Binary files /dev/null and "b/ai-service/models/recognition/exp_images/\354\210\230\355\225\231\353\254\270\354\240\234/\353\235\274\354\235\264\355\212\270\354\216\210 \354\244\221\353\223\261\354\210\230\355\225\231 1-1 - 9_section_03_conf0.44.jpg" differ
diff --git a/docs/CURSOR_SETUP.md b/docs/CURSOR_SETUP.md
new file mode 100644
index 0000000..0b74193
--- /dev/null
+++ b/docs/CURSOR_SETUP.md
@@ -0,0 +1,100 @@
+# Cursor AI 설정 가이드
+
+## Figma MCP 서버 설정
+
+Cursor AI에서 Figma 디자인을 읽고 코드로 변환할 수 있도록 MCP(Model Context Protocol) 서버를 설정하는 방법입니다.
+
+### 1. Figma API 키 발급
+
+1. [Figma 계정 설정](https://www.figma.com/settings)으로 이동
+2. **Personal Access Tokens** 섹션 찾기
+3. **Generate new token** 클릭
+4. 토큰 이름 입력 (예: "Gradi Development")
+5. 생성된 토큰 복사 (⚠️ 한 번만 표시되므로 안전하게 보관!)
+
+### 2. MCP 설정 파일 생성
+
+프로젝트 루트에서 다음 명령 실행:
+
+```bash
+# .cursor 디렉토리 생성 (이미 있으면 생략)
+mkdir -p .cursor
+
+# 템플릿 파일 복사
+cp .cursor/mcp.json.example .cursor/mcp.json
+```
+
+### 3. API 키 설정
+
+`.cursor/mcp.json` 파일을 열고 `YOUR_FIGMA_API_KEY_HERE`를 발급받은 API 키로 교체:
+
+```json
+{
+ "mcpServers": {
+ "Figma": {
+ "command": "npx",
+ "args": ["-y", "figma-developer-mcp", "--stdio"],
+ "env": {
+ "FIGMA_API_KEY": "figd_YOUR_ACTUAL_API_KEY_HERE",
+ "FIGMA_FILE_KEY": "lzHEWBZmLDENjELGjlRkyB",
+ "FIGMA_NODE_ID": "2001:405"
+ }
+ }
+ }
+}
+```
+
+### 4. Cursor 재시작
+
+- Cursor를 완전히 종료하고 다시 시작
+- 또는 Cursor 명령 팔레트(`Cmd+Shift+P` / `Ctrl+Shift+P`)에서 "Reload Window" 실행
+
+### 5. 설정 확인
+
+Cursor AI 채팅에서 다음과 같이 테스트:
+
+```
+@https://www.figma.com/design/lzHEWBZmLDENjELGjlRkyB/Gradi?node-id=2001-405
+이 디자인을 확인해줘
+```
+
+## 주의사항
+
+⚠️ **보안 중요!**
+
+- `.cursor/mcp.json` 파일은 개인 API 키를 포함하므로 **절대 Git에 커밋하지 마세요**
+- `.cursor/` 디렉토리는 이미 `.gitignore`에 포함되어 있습니다
+- API 키는 팀원들과 직접 공유하지 말고, 각자 발급받아 사용하세요
+- API 키가 노출된 경우 즉시 Figma 설정에서 해당 토큰을 삭제하고 새로 발급받으세요
+
+## 프로젝트 정보
+
+- **Figma 파일**: [Gradi Design](https://www.figma.com/design/lzHEWBZmLDENjELGjlRkyB/Gradi)
+- **File Key**: `lzHEWBZmLDENjELGjlRkyB`
+- **Node ID**: `2001:405` (기본값)
+
+## 문제 해결
+
+### MCP 서버가 작동하지 않는 경우
+
+1. Node.js가 설치되어 있는지 확인 (`node --version`)
+2. `npx` 명령어가 작동하는지 확인
+3. Cursor를 완전히 재시작
+4. API 키가 올바른지 확인
+
+### "Invalid API key" 오류
+
+- Figma에서 API 키를 새로 발급받고 교체
+- API 키 앞에 `figd_` 접두사가 있는지 확인
+
+### 권한 오류
+
+- Figma 파일에 대한 접근 권한이 있는지 확인
+- 팀 관리자에게 파일 접근 권한 요청
+
+## 참고 자료
+
+- [Figma Developer API Documentation](https://www.figma.com/developers/api)
+- [MCP Protocol Specification](https://modelcontextprotocol.io/)
+
+
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..3820a95
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/frontend/.metadata b/frontend/.metadata
new file mode 100644
index 0000000..84f56b1
--- /dev/null
+++ b/frontend/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: android
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: ios
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: linux
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: macos
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: web
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: windows
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..26be299
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,16 @@
+# gradi_frontend
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/frontend/analysis_options.yaml b/frontend/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/frontend/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/frontend/android/.gitignore b/frontend/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/frontend/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/frontend/android/app/build.gradle.kts b/frontend/android/app/build.gradle.kts
new file mode 100644
index 0000000..b144a83
--- /dev/null
+++ b/frontend/android/app/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.gradi.gradi_frontend"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.gradi.gradi_frontend"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/frontend/android/app/src/debug/AndroidManifest.xml b/frontend/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/frontend/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1a4a0a
--- /dev/null
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/kotlin/com/gradi/gradi_frontend/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/gradi/gradi_frontend/MainActivity.kt
new file mode 100644
index 0000000..080afd3
--- /dev/null
+++ b/frontend/android/app/src/main/kotlin/com/gradi/gradi_frontend/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.gradi.gradi_frontend
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/frontend/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/drawable/launch_background.xml b/frontend/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/frontend/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/frontend/android/app/src/main/res/values-night/styles.xml b/frontend/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/frontend/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/frontend/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/frontend/android/app/src/profile/AndroidManifest.xml b/frontend/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/frontend/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/frontend/android/build.gradle.kts b/frontend/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/frontend/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/frontend/android/gradle.properties b/frontend/android/gradle.properties
new file mode 100644
index 0000000..f018a61
--- /dev/null
+++ b/frontend/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/frontend/android/gradle/wrapper/gradle-wrapper.properties b/frontend/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ac3b479
--- /dev/null
+++ b/frontend/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
diff --git a/frontend/android/settings.gradle.kts b/frontend/android/settings.gradle.kts
new file mode 100644
index 0000000..fb605bc
--- /dev/null
+++ b/frontend/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.9.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/frontend/assets/images/bookcovers/BookCover_100to100.png b/frontend/assets/images/bookcovers/BookCover_100to100.png
new file mode 100644
index 0000000..b189475
Binary files /dev/null and b/frontend/assets/images/bookcovers/BookCover_100to100.png differ
diff --git a/frontend/assets/images/bookcovers/BookCover_Blacklabel.png b/frontend/assets/images/bookcovers/BookCover_Blacklabel.png
new file mode 100644
index 0000000..78db49f
Binary files /dev/null and b/frontend/assets/images/bookcovers/BookCover_Blacklabel.png differ
diff --git a/frontend/assets/images/bookcovers/BookCover_LightSsen.png b/frontend/assets/images/bookcovers/BookCover_LightSsen.png
new file mode 100644
index 0000000..5efbb3b
Binary files /dev/null and b/frontend/assets/images/bookcovers/BookCover_LightSsen.png differ
diff --git a/frontend/assets/images/bookcovers/workbook_2024.jpg b/frontend/assets/images/bookcovers/workbook_2024.jpg
new file mode 100644
index 0000000..427795a
Binary files /dev/null and b/frontend/assets/images/bookcovers/workbook_2024.jpg differ
diff --git a/frontend/assets/images/bookcovers/workbook_2025.jpg b/frontend/assets/images/bookcovers/workbook_2025.jpg
new file mode 100644
index 0000000..829559e
Binary files /dev/null and b/frontend/assets/images/bookcovers/workbook_2025.jpg differ
diff --git a/frontend/assets/images/bookcovers/workbook_2026.jpg b/frontend/assets/images/bookcovers/workbook_2026.jpg
new file mode 100644
index 0000000..16fb1e1
Binary files /dev/null and b/frontend/assets/images/bookcovers/workbook_2026.jpg differ
diff --git a/frontend/assets/images/icons/Academy_selected.svg b/frontend/assets/images/icons/Academy_selected.svg
new file mode 100644
index 0000000..20e4b8c
--- /dev/null
+++ b/frontend/assets/images/icons/Academy_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Academy_unselected.svg b/frontend/assets/images/icons/Academy_unselected.svg
new file mode 100644
index 0000000..7ff9b66
--- /dev/null
+++ b/frontend/assets/images/icons/Academy_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/Alarm_selected.svg b/frontend/assets/images/icons/Alarm_selected.svg
new file mode 100644
index 0000000..345bf9d
--- /dev/null
+++ b/frontend/assets/images/icons/Alarm_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Alarm_unselected.svg b/frontend/assets/images/icons/Alarm_unselected.svg
new file mode 100644
index 0000000..46d20c8
--- /dev/null
+++ b/frontend/assets/images/icons/Alarm_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/Home_selected.svg b/frontend/assets/images/icons/Home_selected.svg
new file mode 100644
index 0000000..6307829
--- /dev/null
+++ b/frontend/assets/images/icons/Home_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Home_unselected.svg b/frontend/assets/images/icons/Home_unselected.svg
new file mode 100644
index 0000000..ac58144
--- /dev/null
+++ b/frontend/assets/images/icons/Home_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/Mypage_selected.svg b/frontend/assets/images/icons/Mypage_selected.svg
new file mode 100644
index 0000000..2ece542
--- /dev/null
+++ b/frontend/assets/images/icons/Mypage_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Mypage_unselected.svg b/frontend/assets/images/icons/Mypage_unselected.svg
new file mode 100644
index 0000000..e020516
--- /dev/null
+++ b/frontend/assets/images/icons/Mypage_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/Textbook_selected.svg b/frontend/assets/images/icons/Textbook_selected.svg
new file mode 100644
index 0000000..c703dc0
--- /dev/null
+++ b/frontend/assets/images/icons/Textbook_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Textbook_unselected.svg b/frontend/assets/images/icons/Textbook_unselected.svg
new file mode 100644
index 0000000..6b8e8d2
--- /dev/null
+++ b/frontend/assets/images/icons/Textbook_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/Upload_image_selected.svg b/frontend/assets/images/icons/Upload_image_selected.svg
new file mode 100644
index 0000000..4c5e0b9
--- /dev/null
+++ b/frontend/assets/images/icons/Upload_image_selected.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/assets/images/icons/Upload_image_unselected.svg b/frontend/assets/images/icons/Upload_image_unselected.svg
new file mode 100644
index 0000000..b681424
--- /dev/null
+++ b/frontend/assets/images/icons/Upload_image_unselected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/assets/images/icons/bottom_nav_all.png b/frontend/assets/images/icons/bottom_nav_all.png
new file mode 100644
index 0000000..990548a
Binary files /dev/null and b/frontend/assets/images/icons/bottom_nav_all.png differ
diff --git a/frontend/assets/images/icons/bottom_nav_home.png b/frontend/assets/images/icons/bottom_nav_home.png
new file mode 100644
index 0000000..990548a
Binary files /dev/null and b/frontend/assets/images/icons/bottom_nav_home.png differ
diff --git a/frontend/assets/images/icons/nav_home.png b/frontend/assets/images/icons/nav_home.png
new file mode 100644
index 0000000..d303893
Binary files /dev/null and b/frontend/assets/images/icons/nav_home.png differ
diff --git a/frontend/assets/images/icons/nav_workbook.png b/frontend/assets/images/icons/nav_workbook.png
new file mode 100644
index 0000000..7e5a39f
Binary files /dev/null and b/frontend/assets/images/icons/nav_workbook.png differ
diff --git a/frontend/assets/images/social_login/Button_Google.png b/frontend/assets/images/social_login/Button_Google.png
new file mode 100644
index 0000000..57b731b
Binary files /dev/null and b/frontend/assets/images/social_login/Button_Google.png differ
diff --git a/frontend/assets/images/social_login/Button_Kakao.png b/frontend/assets/images/social_login/Button_Kakao.png
new file mode 100644
index 0000000..3e15a1f
Binary files /dev/null and b/frontend/assets/images/social_login/Button_Kakao.png differ
diff --git a/frontend/ios/.gitignore b/frontend/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/frontend/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/frontend/ios/Flutter/AppFrameworkInfo.plist b/frontend/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..1dc6cf7
--- /dev/null
+++ b/frontend/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 13.0
+
+
diff --git a/frontend/ios/Flutter/Debug.xcconfig b/frontend/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/frontend/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/frontend/ios/Flutter/Release.xcconfig b/frontend/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/frontend/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/frontend/ios/Runner.xcodeproj/project.pbxproj b/frontend/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..6600e1d
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,616 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..e3773d4
--- /dev/null
+++ b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/frontend/ios/Runner/AppDelegate.swift b/frontend/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..6266644
--- /dev/null
+++ b/frontend/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..6ed2d93
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cd7b00
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..fe73094
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..502f463
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..e9f5fea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..0467bf1
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner/Base.lproj/Main.storyboard b/frontend/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/frontend/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ios/Runner/Info.plist b/frontend/ios/Runner/Info.plist
new file mode 100644
index 0000000..2b671ed
--- /dev/null
+++ b/frontend/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Gradi Frontend
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ gradi_frontend
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+
diff --git a/frontend/ios/Runner/Runner-Bridging-Header.h b/frontend/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/frontend/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/frontend/ios/RunnerTests/RunnerTests.swift b/frontend/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/frontend/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
new file mode 100644
index 0000000..cd54d66
--- /dev/null
+++ b/frontend/lib/main.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'routes/app_routes.dart';
+import 'theme/app_theme.dart';
+
+void main() {
+ runApp(const GradiApp());
+}
+
+class GradiApp extends StatelessWidget {
+ const GradiApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // Set system UI overlay style
+ SystemChrome.setSystemUIOverlayStyle(
+ const SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.dark,
+ systemNavigationBarColor: Colors.white,
+ systemNavigationBarIconBrightness: Brightness.dark,
+ ),
+ );
+
+ return MaterialApp(
+ title: 'GRADI',
+ debugShowCheckedModeBanner: false,
+ theme: AppTheme.lightTheme,
+
+ // Routing configuration
+ initialRoute: '/login', // login_page.dart 이 페이지로 시작
+ routes: AppRoutes.routes,
+ onGenerateRoute: AppRoutes.onGenerateRoute,
+
+ // Handle unknown routes
+ onUnknownRoute: (settings) =>
+ MaterialPageRoute(builder: (context) => const _UnknownRoutePage()),
+ );
+ }
+}
+
+class _UnknownRoutePage extends StatelessWidget {
+ const _UnknownRoutePage();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('페이지를 찾을 수 없습니다')),
+ body: const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error_outline, size: 64, color: AppTheme.errorColor),
+ SizedBox(height: 16),
+ Text(
+ '요청하신 페이지를 찾을 수 없습니다.',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+ ),
+ SizedBox(height: 8),
+ Text(
+ 'URL을 확인하고 다시 시도해주세요.',
+ style: TextStyle(fontSize: 14, color: AppTheme.textSecondary),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/routes/app_routes.dart b/frontend/lib/routes/app_routes.dart
new file mode 100644
index 0000000..16238dc
--- /dev/null
+++ b/frontend/lib/routes/app_routes.dart
@@ -0,0 +1,113 @@
+import 'package:flutter/material.dart';
+import '../screens/auth/login_page.dart';
+import '../screens/auth/signup_page.dart';
+import '../screens/auth/signup_terms_page.dart';
+import '../screens/auth/signup_success_page.dart';
+import '../screens/auth/reset_password_page.dart';
+import '../screens/auth/password_reset_form_page.dart';
+import '../screens/auth/password_reset_success_page.dart';
+import '../screens/account/find_id_page.dart';
+import '../screens/account/find_id_error_page.dart';
+import '../screens/account/find_id_verification_page.dart';
+import '../screens/account/find_id_result_page.dart';
+import '../screens/account/find_password_page.dart';
+import '../screens/account/find_password_error_page.dart';
+import '../screens/account/find_password_verification_page.dart';
+import '../screens/account/find_password_reset_page.dart';
+import '../screens/main_navigation_page.dart';
+import '../screens/home_page.dart';
+import '../screens/academy/academy_page.dart';
+import '../screens/academy/academy_list_page.dart';
+import '../screens/academy/academy_detail_page.dart';
+import '../screens/workbook/workbook_page.dart';
+
+class AppRoutes {
+ static const String mainNavigation = '/';
+ static const String login = '/login';
+ static const String signup = '/signup';
+ static const String signupTerms = '/signup-terms';
+ static const String signupSuccess = '/signup-success';
+ static const String home = '/home';
+ static const String academy = '/academy';
+ static const String academyList = '/academy/list';
+ static const String academyDetail = '/academy/detail';
+ static const String workbook = '/workbook';
+ static const String findId = '/find-id';
+ static const String findIdError = '/find-id-error';
+ static const String findIdVerification = '/find-id-verification';
+ static const String findIdResult = '/find-id-result';
+ static const String findPassword = '/find-password';
+ static const String findPasswordError = '/find-password-error';
+ static const String findPasswordVerification = '/find-password-verification';
+ static const String findPasswordReset = '/find-password-reset';
+ static const String passwordResetForm = '/password-reset-form';
+ static const String passwordResetSuccess = '/password-reset-success';
+ static const String resetPassword = '/reset-password';
+
+ static Map get routes => {
+ mainNavigation: (context) => const MainNavigationPage(),
+ login: (context) => const LoginPage(),
+ signup: (context) => const SignUpPage(),
+ signupTerms: (context) => const SignUpTermsPage(),
+ signupSuccess: (context) => const SignUpSuccessPage(),
+ home: (context) => const HomePage(),
+ academy: (context) => const AcademyPage(),
+ academyList: (context) => const AcademyListPage(),
+ workbook: (context) => const WorkbookPage(),
+ findId: (context) => const FindIDPage(),
+ findIdError: (context) => const FindIDErrorPage(),
+ findPassword: (context) => const FindPasswordPage(),
+ findPasswordError: (context) => const FindPasswordErrorPage(),
+ };
+
+ static Route? onGenerateRoute(RouteSettings settings) {
+ switch (settings.name) {
+ case findIdVerification:
+ return MaterialPageRoute(
+ builder: (context) => const FindIDVerificationPage(),
+ settings: settings,
+ );
+ case findIdResult:
+ final args = settings.arguments as Map?;
+ return MaterialPageRoute(
+ builder: (context) => FindIDResultPage(
+ userName: args?['userName'] ?? '',
+ userId: args?['userId'] ?? '',
+ ),
+ );
+ case findPasswordVerification:
+ return MaterialPageRoute(
+ builder: (context) => const FindPasswordVerificationPage(),
+ settings: settings,
+ );
+ case resetPassword:
+ return MaterialPageRoute(
+ builder: (context) => const ResetPasswordPage(),
+ settings: settings,
+ );
+ case findPasswordReset:
+ return MaterialPageRoute(
+ builder: (context) => const FindPasswordResetPage(),
+ );
+ case passwordResetForm:
+ return MaterialPageRoute(
+ builder: (context) => const PasswordResetFormPage(),
+ settings: settings,
+ );
+ case passwordResetSuccess:
+ return MaterialPageRoute(
+ builder: (context) => const PasswordResetSuccessPage(),
+ );
+ case academyDetail:
+ final args = settings.arguments as AcademyData?;
+ if (args == null) {
+ return null;
+ }
+ return MaterialPageRoute(
+ builder: (context) => AcademyDetailPage(academy: args),
+ );
+ default:
+ return null;
+ }
+ }
+}
diff --git a/frontend/lib/screens/academy/academy_detail_page.dart b/frontend/lib/screens/academy/academy_detail_page.dart
new file mode 100644
index 0000000..d13eb68
--- /dev/null
+++ b/frontend/lib/screens/academy/academy_detail_page.dart
@@ -0,0 +1,388 @@
+import 'package:flutter/material.dart';
+import 'academy_list_page.dart';
+
+class AcademyDetailPage extends StatelessWidget {
+ final AcademyData academy;
+
+ const AcademyDetailPage({super.key, required this.academy});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 헤더
+ _buildHeader(context),
+
+ // 메인 콘텐츠
+ Expanded(
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 학원 이미지
+ _buildAcademyImage(),
+
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 학원 기본 정보 카드
+ _buildAcademyInfo(),
+
+ const SizedBox(height: 16),
+
+ // 학원 소개
+ _buildAcademyDescription(),
+
+ const SizedBox(height: 32),
+
+ // 등록 버튼
+ _buildRegisterButton(context),
+
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(20, 17, 20, 10),
+ decoration: const BoxDecoration(color: Colors.white),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // 뒤로가기 버튼
+ GestureDetector(
+ onTap: () => Navigator.of(context).pop(),
+ child: const Icon(
+ Icons.arrow_back_ios,
+ color: Color(0xFF333333),
+ size: 24,
+ ),
+ ),
+
+ // 제목 - 학원명과 ID
+ RichText(
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: academy.name,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 18,
+ color: Color(0xFF333333),
+ ),
+ ),
+ TextSpan(
+ text: ' #DF850',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 18,
+ color: Colors.grey[400],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // 햄버거 메뉴
+ IconButton(
+ icon: const Icon(Icons.menu, color: Color(0xFF333333)),
+ onPressed: () {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('메뉴 기능 구현 예정')));
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildAcademyImage() {
+ return Container(
+ width: double.infinity,
+ height: 200,
+ color: const Color(0xFFE0E5EB),
+ child: const Center(
+ child: Text(
+ '학원 이미지 영역',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: Color(0xFF999999),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildAcademyInfo() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withAlpha(13),
+ offset: const Offset(0, 2),
+ blurRadius: 8,
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 주소 섹션
+ _buildInfoRow(Icons.location_on, '주소', academy.address),
+
+ const SizedBox(height: 16),
+ const Divider(color: Color(0xFFE9ECEF), height: 1),
+ const SizedBox(height: 16),
+
+ // 과목 섹션
+ _buildInfoRow(Icons.menu_book, '과목', '초·중등 수학, 영어, 논술'),
+
+ const SizedBox(height: 16),
+ const Divider(color: Color(0xFFE9ECEF), height: 1),
+ const SizedBox(height: 16),
+
+ // 영업시간 섹션
+ _buildOperatingHours(),
+
+ const SizedBox(height: 16),
+ const Divider(color: Color(0xFFE9ECEF), height: 1),
+ const SizedBox(height: 16),
+
+ // 연락처 섹션
+ _buildContactSection(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildInfoRow(IconData icon, String label, String value) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: const Color(0xFF666666), size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ label,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ value,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildOperatingHours() {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Icon(Icons.access_time, color: Color(0xFF666666), size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '영업시간',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 4),
+ const Text(
+ '월요일 - 금요일 : 9:00 AM - 8:00 PM',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ const SizedBox(height: 2),
+ const Text(
+ '토요일 : 10:00 AM - 6:00 PM',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ const SizedBox(height: 2),
+ const Text(
+ '일요일 : 12:00 PM - 5:00 PM',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildContactSection() {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Icon(Icons.phone, color: Color(0xFF666666), size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '연락처',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 4),
+ const Text(
+ 'Phone: (555) 123-4567',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ const SizedBox(height: 2),
+ const Text(
+ 'Email: info@grandlibrary.org',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ const SizedBox(height: 2),
+ const Text(
+ 'Website: www.grandlibrary.org',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildAcademyDescription() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ '${academy.name}은(는) 입시와 공무원 시험에 특화된 전문 교육 기관입니다. 체계적인 커리큘럼과 1:1 맞춤형 학습 관리로 높은 합격률을 자랑합니다. 단국대학교 죽전캠퍼스에서 ${academy.distance} 거리에 위치하고 있습니다.',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ height: 1.6,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildRegisterButton(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ height: 56,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF7C3AED)],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: ElevatedButton(
+ onPressed: () {
+ // TODO: 학원 등록 API 호출
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('${academy.name} 등록 요청 (구현 예정)')),
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: const Text(
+ '학원 등록하기',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 16,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/academy/academy_list_page.dart b/frontend/lib/screens/academy/academy_list_page.dart
new file mode 100644
index 0000000..667d09d
--- /dev/null
+++ b/frontend/lib/screens/academy/academy_list_page.dart
@@ -0,0 +1,394 @@
+import 'package:flutter/material.dart';
+import '../../widgets/back_button.dart';
+
+class AcademyListPage extends StatefulWidget {
+ const AcademyListPage({super.key});
+
+ @override
+ State createState() => _AcademyListPageState();
+}
+
+class _AcademyListPageState extends State {
+ final TextEditingController _searchController = TextEditingController();
+ List _filteredAcademies = [];
+ List _allAcademies = [];
+
+ @override
+ void initState() {
+ super.initState();
+ _initializeAcademies();
+ _filteredAcademies = _allAcademies;
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+ // TODO: Implement logic to fetch information of 20 nearby academies from the database.
+ // Details:
+ // - The academies should be displayed in order of proximity.
+ // - Implement a search functionality where typing "이투스" in the search bar
+ // will display all academies with "이투스" in their name.
+
+ void _initializeAcademies() {
+ _allAcademies = [
+ AcademyData(
+ name: '정다훈 영어학원',
+ distance: '0.5km',
+ address: '경기도 용인시 기흥구 죽전로 152',
+ thumbnail: 'assets/images/academy1.jpg',
+ ),
+ AcademyData(
+ name: '청담어학원 죽전캠퍼스',
+ distance: '0.8km',
+ address: '경기도 용인시 기흥구 죽전로 200',
+ thumbnail: 'assets/images/academy2.jpg',
+ ),
+ AcademyData(
+ name: '메가스터디 영어학원',
+ distance: '1.2km',
+ address: '경기도 용인시 기흥구 죽전로 300',
+ thumbnail: 'assets/images/academy3.jpg',
+ ),
+ AcademyData(
+ name: '대성마이맥 영어학원',
+ distance: '1.5km',
+ address: '경기도 용인시 기흥구 신갈로 100',
+ thumbnail: 'assets/images/academy4.jpg',
+ ),
+ AcademyData(
+ name: '이투스 영어학원',
+ distance: '2.0km',
+ address: '경기도 용인시 기흥구 신갈로 200',
+ thumbnail: 'assets/images/academy5.jpg',
+ ),
+ AcademyData(
+ name: '청심영어학원',
+ distance: '2.3km',
+ address: '경기도 용인시 기흥구 보정로 50',
+ thumbnail: 'assets/images/academy6.jpg',
+ ),
+ AcademyData(
+ name: '윤선생 영어학원',
+ distance: '2.8km',
+ address: '경기도 용인시 기흥구 보정로 150',
+ thumbnail: 'assets/images/academy7.jpg',
+ ),
+ AcademyData(
+ name: '파고다 영어학원',
+ distance: '3.1km',
+ address: '경기도 용인시 기흥구 보정로 250',
+ thumbnail: 'assets/images/academy8.jpg',
+ ),
+ AcademyData(
+ name: 'YBM 영어학원',
+ distance: '3.5km',
+ address: '경기도 용인시 기흥구 구갈로 100',
+ thumbnail: 'assets/images/academy9.jpg',
+ ),
+ AcademyData(
+ name: '스터디포스 영어학원',
+ distance: '4.0km',
+ address: '경기도 용인시 기흥구 구갈로 200',
+ thumbnail: 'assets/images/academy10.jpg',
+ ),
+ AcademyData(
+ name: '글로벌어학원',
+ distance: '4.2km',
+ address: '경기도 용인시 기흥구 신갈로 300',
+ thumbnail: 'assets/images/academy11.jpg',
+ ),
+ AcademyData(
+ name: '어학원 스카이',
+ distance: '4.8km',
+ address: '경기도 용인시 기흥구 신갈로 400',
+ thumbnail: 'assets/images/academy12.jpg',
+ ),
+ AcademyData(
+ name: '영어마을학원',
+ distance: '5.1km',
+ address: '경기도 용인시 기흥구 죽전로 500',
+ thumbnail: 'assets/images/academy13.jpg',
+ ),
+ AcademyData(
+ name: '토익마스터 학원',
+ distance: '5.5km',
+ address: '경기도 용인시 기흥구 죽전로 600',
+ thumbnail: 'assets/images/academy14.jpg',
+ ),
+ AcademyData(
+ name: '토플전문학원',
+ distance: '6.0km',
+ address: '경기도 용인시 기흥구 죽전로 700',
+ thumbnail: 'assets/images/academy15.jpg',
+ ),
+ ];
+ }
+
+ void _onSearchChanged(String query) {
+ setState(() {
+ if (query.isEmpty) {
+ _filteredAcademies = _allAcademies;
+ } else {
+ _filteredAcademies = _allAcademies
+ .where(
+ (academy) =>
+ academy.name.toLowerCase().contains(query.toLowerCase()),
+ )
+ .toList();
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 헤더
+ _buildHeader(),
+
+ // 메인 콘텐츠
+ Expanded(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.05,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ height: MediaQuery.of(context).size.height * 0.03,
+ ),
+
+ // 검색바
+ _buildSearchBar(),
+
+ Container(
+ height: MediaQuery.of(context).size.height * 0.03,
+ ),
+
+ // 학원 목록
+ _buildAcademyList(),
+
+ Container(
+ height: MediaQuery.of(context).size.height * 0.025,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ MediaQuery.of(context).size.width * 0.05,
+ MediaQuery.of(context).size.height * 0.021,
+ MediaQuery.of(context).size.width * 0.05,
+ MediaQuery.of(context).size.height * 0.012,
+ ),
+ decoration: const BoxDecoration(color: Colors.white),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const CustomBackButton(),
+ const SizedBox(width: 20),
+ const Text(
+ '학원 등록하기',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 20,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ],
+ ),
+ IconButton(
+ icon: const Icon(Icons.menu, color: Color(0xFF333333)),
+ onPressed: () {
+ // TODO: 메뉴 기능 구현
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('메뉴 기능 구현 예정')));
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSearchBar() {
+ return Container(
+ constraints: BoxConstraints(
+ maxHeight: MediaQuery.of(context).size.height * 0.045,
+ minHeight: 30,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(50),
+ ),
+ child: TextField(
+ controller: _searchController,
+ onChanged: _onSearchChanged,
+ decoration: const InputDecoration(
+ hintText: '학원 이름을 등록해주세요',
+ hintStyle: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
+ color: Color(0xFF666666),
+ ),
+ prefixIcon: Icon(Icons.search, color: Color(0xFF666666), size: 23),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(
+ horizontal: 22,
+ vertical: 11,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildAcademyList() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 섹션 제목
+ const Text(
+ '현위치 근처에 있는 학원이에요',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // 학원 카드 목록
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _filteredAcademies.length,
+ separatorBuilder: (context, index) =>
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+ itemBuilder: (context, index) {
+ return _buildAcademyCard(_filteredAcademies[index]);
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildAcademyCard(AcademyData academy) {
+ return GestureDetector(
+ onTap: () {
+ Navigator.pushNamed(context, '/academy/detail', arguments: academy);
+ },
+ child: Container(
+ padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.025),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Row(
+ children: [
+ // 학원 썸네일
+ Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.25,
+ minWidth: 80,
+ maxHeight: MediaQuery.of(context).size.width * 0.25,
+ minHeight: 80,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFF666666),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: const Icon(Icons.school, color: Colors.white, size: 40),
+ ),
+
+ Container(width: MediaQuery.of(context).size.width * 0.04),
+
+ // 학원 정보
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 학원명
+ Text(
+ academy.name,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.035),
+
+ // 거리와 주소
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ academy.distance,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFFADADAD),
+ ),
+ ),
+ Container(
+ height: MediaQuery.of(context).size.height * 0.005,
+ ),
+ Text(
+ academy.address,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFFADADAD),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class AcademyData {
+ final String name;
+ final String distance;
+ final String address;
+ final String thumbnail;
+
+ AcademyData({
+ required this.name,
+ required this.distance,
+ required this.address,
+ required this.thumbnail,
+ });
+}
diff --git a/frontend/lib/screens/academy/academy_page.dart b/frontend/lib/screens/academy/academy_page.dart
new file mode 100644
index 0000000..fe7d3c6
--- /dev/null
+++ b/frontend/lib/screens/academy/academy_page.dart
@@ -0,0 +1,263 @@
+import 'package:flutter/material.dart';
+
+class AcademyPage extends StatefulWidget {
+ const AcademyPage({super.key});
+
+ @override
+ State createState() => _AcademyPageState();
+}
+
+class _AcademyPageState extends State {
+ // TODO: 서버에서 등록된 학원 목록을 불러오는 로직 구현
+ // 임시로 빈 리스트 사용
+ final List _registeredAcademies = [];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 헤더
+ _buildHeader(),
+
+ // 메인 콘텐츠
+ Expanded(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ const SizedBox(height: 18),
+
+ // 학원 목록 또는 빈 상태
+ _registeredAcademies.isEmpty
+ ? _buildEmptyState()
+ : _buildAcademyList(),
+
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(30, 17, 30, 17),
+ decoration: const BoxDecoration(
+ color: Color(0xFFF8F9FA),
+ boxShadow: [
+ BoxShadow(
+ color: Color(0x1A000000),
+ blurRadius: 4,
+ offset: Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const SizedBox(width: 24), // 시각적 균형을 위한 공간
+ const Text(
+ '학원',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 20,
+ color: Color(0xFF585B69),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.menu, color: Color(0xFF585B69), size: 24),
+ onPressed: () {
+ // TODO: 메뉴 기능 구현
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('메뉴 기능 구현 예정')));
+ },
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildEmptyState() {
+ return Column(
+ children: [
+ // "등록된 학원이 없습니다." 카드
+ Container(
+ width: double.infinity,
+ height: 81,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE1E7ED)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Center(
+ child: Text(
+ '등록된 학원이 없습니다.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF585B69),
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 15),
+
+ // "+" 버튼 카드
+ GestureDetector(
+ onTap: () {
+ Navigator.pushNamed(context, '/academy/list');
+ },
+ child: Container(
+ width: double.infinity,
+ height: 46,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE1E7ED)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Center(
+ child: Icon(Icons.add, size: 24, color: Color(0xFF585B69)),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildAcademyList() {
+ return Column(
+ children: [
+ // 등록된 학원 카드 목록
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _registeredAcademies.length,
+ separatorBuilder: (context, index) => const SizedBox(height: 16),
+ itemBuilder: (context, index) {
+ return _buildAcademyCard(_registeredAcademies[index]);
+ },
+ ),
+
+ const SizedBox(height: 16),
+
+ // "+" 버튼 카드
+ GestureDetector(
+ onTap: () {
+ Navigator.pushNamed(context, '/academy/list');
+ },
+ child: Container(
+ width: double.infinity,
+ height: 46,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE1E7ED)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Center(
+ child: Icon(Icons.add, size: 24, color: Color(0xFF585B69)),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildAcademyCard(AcademyItem academy) {
+ return Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE1E7ED)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Row(
+ children: [
+ // 학원 썸네일
+ Container(
+ width: 97,
+ height: 97,
+ decoration: BoxDecoration(
+ color: const Color(0xFFE1E7ED),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: const Icon(Icons.school, color: Colors.white, size: 40),
+ ),
+
+ const SizedBox(width: 15),
+
+ // 학원 정보
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 학원명
+ Text(
+ academy.name,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF585B69),
+ ),
+ ),
+
+ const SizedBox(height: 28),
+
+ // 거리
+ Text(
+ academy.distance,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFF585B69),
+ ),
+ ),
+
+ const SizedBox(height: 4),
+
+ // 주소
+ Text(
+ academy.address,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFF585B69),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class AcademyItem {
+ final String name;
+ final String distance;
+ final String address;
+ final String? thumbnail;
+
+ AcademyItem({
+ required this.name,
+ required this.distance,
+ required this.address,
+ this.thumbnail,
+ });
+}
diff --git a/frontend/lib/screens/account/find_id_error_page.dart b/frontend/lib/screens/account/find_id_error_page.dart
new file mode 100644
index 0000000..4325e26
--- /dev/null
+++ b/frontend/lib/screens/account/find_id_error_page.dart
@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/next_button.dart';
+
+class FindIDErrorPage extends StatefulWidget {
+ const FindIDErrorPage({super.key});
+
+ @override
+ State createState() => _FindIDErrorPageState();
+}
+
+class _FindIDErrorPageState extends State {
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ void _handleRetry() {
+ Navigator.pop(context); // Go back to FindIDPage to retry
+ }
+
+ void _handleBackToLogin() {
+ Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '아이디 찾기'),
+ ],
+ ),
+ const SizedBox(height: 40),
+ const Icon(
+ Icons.error_outline,
+ size: 64,
+ color: Color(0xFFFF4258),
+ ),
+ const SizedBox(height: 24),
+ const Text(
+ '입력한 이메일로\n아이디를 찾을 수 없습니다.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 18,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ '입력하신 정보를 다시 확인해주세요.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ const SizedBox(height: 40),
+ NextButton(text: '다시 시도', onPressed: _handleRetry),
+ const SizedBox(height: 16),
+ TextButton(
+ onPressed: _handleBackToLogin,
+ child: const Text(
+ '로그인으로 돌아가기',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ color: Color(0xFF666EDE),
+ ),
+ ),
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/account/find_id_page.dart b/frontend/lib/screens/account/find_id_page.dart
new file mode 100644
index 0000000..27b7259
--- /dev/null
+++ b/frontend/lib/screens/account/find_id_page.dart
@@ -0,0 +1,252 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/labeled_input_field.dart';
+import '../../widgets/next_button.dart';
+
+class FindIDPage extends StatefulWidget {
+ const FindIDPage({super.key});
+
+ @override
+ State createState() => _FindIDPageState();
+}
+
+class _FindIDPageState extends State {
+ final TextEditingController _nameController = TextEditingController();
+ final TextEditingController _emailController = TextEditingController();
+
+ // 유효성 검사 상태
+ String? _nameError;
+ String? _emailError;
+ bool _isNameFieldError = false;
+ bool _isEmailFieldError = false;
+ bool _isLoading = false;
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _emailController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ Navigator.of(context).pop();
+ }
+
+ bool _isValidEmail(String email) {
+ // 이메일 형식 검증: @가 포함되고 앞뒤로 최소 한 글자 이상
+ final emailRegex = RegExp(r'^.+@.+$');
+ return emailRegex.hasMatch(email);
+ }
+
+ void _clearErrors() {
+ setState(() {
+ _nameError = null;
+ _emailError = null;
+ _isNameFieldError = false;
+ _isEmailFieldError = false;
+ });
+ }
+
+ bool _validateInputs() {
+ bool isValid = true;
+ _clearErrors();
+
+ // 이름 유효성 검사
+ if (_nameController.text.trim().isEmpty) {
+ setState(() {
+ _nameError = '이름을 입력해주세요.';
+ _isNameFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 이메일 유효성 검사
+ if (_emailController.text.trim().isEmpty) {
+ setState(() {
+ _emailError = '이메일을 입력해주세요.';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ } else if (!_isValidEmail(_emailController.text.trim())) {
+ setState(() {
+ _emailError = '올바른 주소를 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ Future _handleNext() async {
+ if (!_validateInputs()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/send-code/find_account';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'name': _nameController.text.trim(),
+ 'email': _emailController.text.trim(),
+ };
+
+ developer.log(
+ 'Find ID attempted with name: ${_nameController.text.trim()}',
+ );
+ developer.log('Sending POST request to: $url');
+ developer.log('Request data: $requestData');
+
+ // HTTP POST 요청
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+
+ if (!mounted) return;
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+ developer.log(
+ 'Find ID request successful. Response data: $responseData',
+ );
+
+ // success 컬럼이 true인지 확인
+ if (responseData['success'] == true) {
+ // 성공 시 인증 페이지로 이동
+ final userName = _nameController.text.trim();
+ final email = _emailController.text.trim();
+ developer.log(
+ 'Navigating to verification page with userName: $userName, email: $email',
+ );
+ Navigator.pushNamed(
+ context,
+ '/find-id-verification',
+ arguments: {'userName': userName, 'email': email},
+ );
+ } else {
+ // success가 false인 경우 에러 페이지로 이동
+ developer.log('Find ID failed: success is false');
+ Navigator.pushNamed(context, '/find-id-error');
+ }
+ } else if (response.statusCode == 500) {
+ // 500 에러 시 에러 페이지로 이동
+ developer.log('Find ID failed: User not found (500 error)');
+ if (!mounted) return;
+ Navigator.pushNamed(context, '/find-id-error');
+ } else {
+ // 기타 에러
+ developer.log('Find ID failed with status: ${response.statusCode}');
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('오류가 발생했습니다: ${response.statusCode}')),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ developer.log('Find ID error: $e');
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('네트워크 오류: $e')));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '아이디 찾기'),
+ ],
+ ),
+ const SizedBox(height: 20),
+ LabeledInputField(
+ label: '이름*',
+ placeholder: '이름을 입력해주세요',
+ controller: _nameController,
+ keyboardType: TextInputType.text,
+ isError: _isNameFieldError,
+ errorMessage: _nameError,
+ ),
+ const SizedBox(height: 24),
+ LabeledInputField(
+ label: '이메일*',
+ placeholder: '이메일 주소를 입력해주세요',
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ isError: _isEmailFieldError,
+ errorMessage: _emailError,
+ ),
+ const SizedBox(height: 20),
+ NextButton(
+ text: _isLoading ? '처리 중...' : '다음',
+ onPressed: _isLoading ? null : _handleNext,
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/account/find_id_result_page.dart b/frontend/lib/screens/account/find_id_result_page.dart
new file mode 100644
index 0000000..f110a19
--- /dev/null
+++ b/frontend/lib/screens/account/find_id_result_page.dart
@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/user_id_card.dart';
+import '../../widgets/login_button.dart';
+
+class FindIDResultPage extends StatefulWidget {
+ final String userName;
+ final String userId;
+
+ const FindIDResultPage({
+ super.key,
+ required this.userName,
+ required this.userId,
+ });
+
+ @override
+ State createState() => _FindIDResultPageState();
+}
+
+class _FindIDResultPageState extends State {
+ String? _userName;
+ String? _userId;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // arguments에서 데이터 추출
+ final args =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null) {
+ _userName = args['userName'] as String?;
+ _userId = args['userId'] as String?;
+ }
+ }
+
+ void _handleBack() {
+ Navigator.of(context).pop();
+ }
+
+ void _handleLogin() {
+ // Navigate to login page
+ Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final screenHeight =
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom;
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(minHeight: screenHeight),
+ child: IntrinsicHeight(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 20),
+ // Back Button and Title
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '아이디 찾기'),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+ // User ID Card
+ UserIDCard(
+ userName: _userName ?? widget.userName,
+ userId: _userId ?? widget.userId,
+ ),
+
+ const SizedBox(height: 100), // Spacing before button
+ // Login Button
+ Padding(
+ padding: const EdgeInsets.only(bottom: 29),
+ child: LoginButton(
+ text: '로그인하기',
+ onPressed: _handleLogin,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/account/find_id_verification_page.dart b/frontend/lib/screens/account/find_id_verification_page.dart
new file mode 100644
index 0000000..308a571
--- /dev/null
+++ b/frontend/lib/screens/account/find_id_verification_page.dart
@@ -0,0 +1,283 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/verification_code_input.dart';
+import '../../widgets/next_button.dart';
+
+class FindIDVerificationPage extends StatefulWidget {
+ const FindIDVerificationPage({super.key});
+
+ @override
+ State createState() => _FindIDVerificationPageState();
+}
+
+class _FindIDVerificationPageState extends State {
+ String _verificationCode = '';
+ bool _isLoading = false;
+ String? _userName;
+ String? _email;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // arguments에서 데이터 추출
+ final args =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null) {
+ _userName = args['userName'] as String?;
+ _email = args['email'] as String?;
+ developer.log(
+ 'FindIDVerificationPage - userName: $_userName, email: $_email',
+ );
+ developer.log('FindIDVerificationPage - args: $args');
+ } else {
+ developer.log('FindIDVerificationPage - No arguments received');
+ }
+ }
+
+ void _handleBack() {
+ Navigator.of(context).pop();
+ }
+
+ void _handleNext() async {
+ if (_verificationCode.length != 6) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('6자리 인증번호를 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (mounted) {
+ setState(() {
+ _isLoading = true;
+ });
+ }
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/verify/find_account';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'email': _email ?? '',
+ 'code': _verificationCode.toString(),
+ };
+
+ developer.log('Sending verification request: $requestData');
+ developer.log('Request URL: $url');
+ developer.log('Email before request: $_email');
+ developer.log('Verification code: $_verificationCode');
+ developer.log(
+ 'Request headers: {\'Content-Type\': \'application/json\'}',
+ );
+
+ // API 호출
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response headers: ${response.headers}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+
+ if (responseData['success'] == true) {
+ // 성공 시 결과 페이지로 이동
+ Navigator.pushNamed(
+ context,
+ '/find-id-result',
+ arguments: {
+ 'userName': responseData['data']['name'] ?? _userName ?? '',
+ 'userId': responseData['data']['account_id'] ?? '',
+ },
+ );
+ } else {
+ // 인증 실패
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('인증번호가 올바르지 않습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } else {
+ // 서버 오류
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('서버 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } catch (e) {
+ developer.log('Error during verification: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('네트워크 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ void _handleResendCode() {
+ // Handle resend code logic here
+ developer.log('Resend code requested');
+ }
+
+ void _onCodeChanged(String code) {
+ if (mounted) {
+ setState(() {
+ _verificationCode = code;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 20),
+ // Back Button and Title
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '아이디 찾기'),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+ // Welcome Message
+ Text(
+ '${_userName ?? ''}님, 환영합니다.',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF5C5C5C),
+ height: 1.193,
+ ),
+ ),
+
+ const SizedBox(height: 7),
+ // Instructions
+ const Text(
+ '이메일로 보내드린 6자 코드를 입력해주세요.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF5C5C5C),
+ height: 1.193,
+ ),
+ ),
+
+ const SizedBox(height: 24),
+ // Verification Code Input
+ VerificationCodeInput(
+ length: 6,
+ onChanged: _onCodeChanged,
+ width: 320,
+ height: 50,
+ ),
+
+ const SizedBox(height: 13),
+ // Resend Code Link
+ GestureDetector(
+ onTap: _handleResendCode,
+ child: RichText(
+ text: TextSpan(
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFFADADAD),
+ height: 1.193,
+ ),
+ children: [
+ const TextSpan(text: '인증번호를 받지 못했나요? '),
+ TextSpan(
+ text: '재전송',
+ style: TextStyle(
+ color: const Color(0xFFADADAD),
+ decoration: TextDecoration.underline,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 161),
+ // Next Button
+ NextButton(
+ text: _isLoading ? '처리 중...' : '다음',
+ onPressed: (_verificationCode.length == 6 && !_isLoading)
+ ? _handleNext
+ : null,
+ ),
+
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/account/find_password_error_page.dart b/frontend/lib/screens/account/find_password_error_page.dart
new file mode 100644
index 0000000..b76fd7e
--- /dev/null
+++ b/frontend/lib/screens/account/find_password_error_page.dart
@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/next_button.dart';
+
+class FindPasswordErrorPage extends StatefulWidget {
+ const FindPasswordErrorPage({super.key});
+
+ @override
+ State createState() => _FindPasswordErrorPageState();
+}
+
+class _FindPasswordErrorPageState extends State {
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ void _handleRetry() {
+ Navigator.pop(context); // Go back to FindPasswordPage to retry
+ }
+
+ void _handleBackToLogin() {
+ Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '비밀번호 변경'),
+ ],
+ ),
+ const SizedBox(height: 40),
+ const Icon(
+ Icons.error_outline,
+ size: 64,
+ color: Color(0xFFFF4258),
+ ),
+ const SizedBox(height: 24),
+ const Text(
+ '입력한 정보로\n비밀번호를 찾을 수 없습니다.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 18,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ '입력하신 정보를 다시 확인해주세요.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ const SizedBox(height: 40),
+ NextButton(text: '다시 시도', onPressed: _handleRetry),
+ const SizedBox(height: 16),
+ TextButton(
+ onPressed: _handleBackToLogin,
+ child: const Text(
+ '로그인으로 돌아가기',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ color: Color(0xFF666EDE),
+ ),
+ ),
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/account/find_password_page.dart b/frontend/lib/screens/account/find_password_page.dart
new file mode 100644
index 0000000..d3eb46b
--- /dev/null
+++ b/frontend/lib/screens/account/find_password_page.dart
@@ -0,0 +1,277 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/labeled_input_field.dart';
+import '../../widgets/next_button.dart';
+
+class FindPasswordPage extends StatefulWidget {
+ const FindPasswordPage({super.key});
+
+ @override
+ State createState() => _FindPasswordPageState();
+}
+
+class _FindPasswordPageState extends State {
+ final TextEditingController _nameController = TextEditingController();
+ final TextEditingController _emailController = TextEditingController();
+ final TextEditingController _idController = TextEditingController();
+
+ // 유효성 검사 상태
+ String? _nameError;
+ String? _emailError;
+ String? _idError;
+ bool _isNameFieldError = false;
+ bool _isEmailFieldError = false;
+ bool _isIdFieldError = false;
+ bool _isLoading = false;
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _emailController.dispose();
+ _idController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ bool _isValidEmail(String email) {
+ // 이메일 형식 검증: @가 포함되고 앞뒤로 최소 한 글자 이상
+ final emailRegex = RegExp(r'^.+@.+$');
+ return emailRegex.hasMatch(email);
+ }
+
+ void _clearErrors() {
+ setState(() {
+ _nameError = null;
+ _emailError = null;
+ _idError = null;
+ _isNameFieldError = false;
+ _isEmailFieldError = false;
+ _isIdFieldError = false;
+ });
+ }
+
+ bool _validateInputs() {
+ bool isValid = true;
+ _clearErrors();
+
+ // 이름 유효성 검사
+ if (_nameController.text.trim().isEmpty) {
+ setState(() {
+ _nameError = '이름을 입력해주세요.';
+ _isNameFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 이메일 유효성 검사
+ if (_emailController.text.trim().isEmpty) {
+ setState(() {
+ _emailError = '이메일을 입력해주세요.';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ } else if (!_isValidEmail(_emailController.text.trim())) {
+ setState(() {
+ _emailError = '올바른 주소를 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 아이디 유효성 검사
+ if (_idController.text.trim().isEmpty) {
+ setState(() {
+ _idError = '아이디를 입력해주세요.';
+ _isIdFieldError = true;
+ });
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ Future _handleNext() async {
+ if (!_validateInputs()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/send-code/reset_password';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'name': _nameController.text.trim(),
+ 'email': _emailController.text.trim(),
+ 'account_id': _idController.text.trim(),
+ };
+
+ developer.log(
+ 'Find Password attempted with name: ${_nameController.text.trim()}',
+ );
+ developer.log('Sending POST request to: $url');
+ developer.log('Request data: $requestData');
+
+ // HTTP POST 요청
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ setState(() {
+ _isLoading = false;
+ });
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+ developer.log(
+ 'Find Password request successful. Response data: $responseData',
+ );
+
+ // success 컬럼이 true인지 확인
+ if (responseData['success'] == true) {
+ // 성공 시 비밀번호 인증 페이지로 이동
+ Navigator.pushNamed(
+ context,
+ '/find-password-verification',
+ arguments: {
+ 'userName': _nameController.text.trim(),
+ 'userId': _idController.text.trim(),
+ 'email': _emailController.text.trim(),
+ },
+ );
+ } else {
+ // success가 false인 경우 에러 페이지로 이동
+ developer.log('Find Password failed: success is false');
+ Navigator.pushNamed(context, '/find-password-error');
+ }
+ } else if (response.statusCode == 500) {
+ // 500 에러 시 에러 페이지로 이동
+ developer.log('Find Password failed: User not found (500 error)');
+ Navigator.pushNamed(context, '/find-password-error');
+ } else {
+ // 기타 에러
+ developer.log(
+ 'Find Password failed with status: ${response.statusCode}',
+ );
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('오류가 발생했습니다: ${response.statusCode}')),
+ );
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ developer.log('Find Password error: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('네트워크 오류: $e')));
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '비밀번호 변경'),
+ ],
+ ),
+ const SizedBox(height: 20),
+ LabeledInputField(
+ label: '이름*',
+ placeholder: '이름을 입력해주세요',
+ controller: _nameController,
+ keyboardType: TextInputType.text,
+ isError: _isNameFieldError,
+ errorMessage: _nameError,
+ ),
+ const SizedBox(height: 24),
+ LabeledInputField(
+ label: '이메일*',
+ placeholder: '이메일 주소를 입력해주세요',
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ isError: _isEmailFieldError,
+ errorMessage: _emailError,
+ ),
+ const SizedBox(height: 24),
+ LabeledInputField(
+ label: '아이디*',
+ placeholder: '아이디를 입력해주세요',
+ controller: _idController,
+ keyboardType: TextInputType.text,
+ isError: _isIdFieldError,
+ errorMessage: _idError,
+ ),
+ const SizedBox(height: 20),
+ NextButton(
+ text: _isLoading ? '처리 중...' : '다음',
+ onPressed: _isLoading ? null : _handleNext,
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/account/find_password_reset_page.dart b/frontend/lib/screens/account/find_password_reset_page.dart
new file mode 100644
index 0000000..4989dff
--- /dev/null
+++ b/frontend/lib/screens/account/find_password_reset_page.dart
@@ -0,0 +1,392 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/labeled_input_field.dart';
+import '../../widgets/next_button.dart';
+
+class FindPasswordResetPage extends StatefulWidget {
+ const FindPasswordResetPage({super.key});
+
+ @override
+ State createState() => _FindPasswordResetPageState();
+}
+
+class _FindPasswordResetPageState extends State {
+ final TextEditingController _newPasswordController = TextEditingController();
+ final TextEditingController _confirmPasswordController =
+ TextEditingController();
+
+ // 유효성 검사 상태
+ String? _newPasswordError;
+ String? _confirmPasswordError;
+ bool _isNewPasswordFieldError = false;
+ bool _isConfirmPasswordFieldError = false;
+ bool _isLoading = false;
+
+ // 비밀번호 정책 검사 결과
+ List _passwordPolicyErrors = [];
+
+ // 전달받은 사용자 정보
+ String? _userId;
+
+ @override
+ void initState() {
+ super.initState();
+ // 전달받은 arguments 처리
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ final arguments =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (arguments != null) {
+ setState(() {
+ _userId = arguments['userId'];
+ });
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _newPasswordController.dispose();
+ _confirmPasswordController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ // 비밀번호 찾기 페이지로 이동 (입력폼은 모두 공백 상태)
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/find-password',
+ (route) => false,
+ );
+ }
+
+ // 비밀번호 정책 검사
+ List _validatePasswordPolicy(String password) {
+ List errors = [];
+
+ if (password.length < 8) {
+ errors.add('8자 이상이어야 합니다');
+ }
+ if (!RegExp(r'[A-Z]').hasMatch(password)) {
+ errors.add('대문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[a-z]').hasMatch(password)) {
+ errors.add('소문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[0-9]').hasMatch(password)) {
+ errors.add('숫자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
+ errors.add('특수문자를 포함해야 합니다');
+ }
+
+ return errors;
+ }
+
+ // 실시간 비밀번호 유효성 검사
+ void _onNewPasswordChanged(String value) {
+ setState(() {
+ _passwordPolicyErrors = _validatePasswordPolicy(value);
+ _newPasswordError = null;
+ _isNewPasswordFieldError = false;
+ });
+
+ // 비밀번호 확인 필드도 실시간으로 검사
+ if (_confirmPasswordController.text.isNotEmpty) {
+ _onConfirmPasswordChanged(_confirmPasswordController.text);
+ }
+ }
+
+ // 실시간 비밀번호 확인 검사
+ void _onConfirmPasswordChanged(String value) {
+ setState(() {
+ if (value.isNotEmpty && _newPasswordController.text != value) {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ } else {
+ _confirmPasswordError = null;
+ _isConfirmPasswordFieldError = false;
+ }
+ });
+ }
+
+ void _clearErrors() {
+ setState(() {
+ _newPasswordError = null;
+ _confirmPasswordError = null;
+ _isNewPasswordFieldError = false;
+ _isConfirmPasswordFieldError = false;
+ _passwordPolicyErrors = [];
+ });
+ }
+
+ bool _validateInputs() {
+ bool isValid = true;
+ _clearErrors();
+
+ // 새 비밀번호 유효성 검사
+ if (_newPasswordController.text.trim().isEmpty) {
+ setState(() {
+ _newPasswordError = '변경할 비밀번호를 입력해주세요';
+ _isNewPasswordFieldError = true;
+ });
+ isValid = false;
+ } else {
+ // 비밀번호 정책 검사
+ List policyErrors = _validatePasswordPolicy(
+ _newPasswordController.text,
+ );
+ if (policyErrors.isNotEmpty) {
+ setState(() {
+ _passwordPolicyErrors = policyErrors;
+ });
+ isValid = false;
+ }
+ }
+
+ // 비밀번호 확인 유효성 검사
+ if (_confirmPasswordController.text.trim().isEmpty) {
+ setState(() {
+ _isConfirmPasswordFieldError = true; // 테두리만 빨간색으로, 에러 메시지는 표시하지 않음
+ });
+ isValid = false;
+ } else if (_newPasswordController.text != _confirmPasswordController.text) {
+ setState(() {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ });
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ Future _handleNext() async {
+ if (!_validateInputs()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/change/reset_password';
+
+ // TODO: JSON 형식 미정 - 서버 API 스펙에 맞게 수정 필요
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'account_id': _userId ?? '',
+ 'new_password': _newPasswordController.text.trim(),
+ };
+
+ developer.log('Password reset attempted for user: $_userId');
+ developer.log('Sending POST request to: $url');
+ developer.log('Request data: $requestData');
+
+ // HTTP POST 요청
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+
+ if (!mounted) return;
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+ developer.log(
+ 'Password reset successful. Response data: $responseData',
+ );
+
+ // success 컬럼이 true인지 확인
+ if (responseData['success'] == true) {
+ // 성공 페이지로 이동
+ Navigator.pushNamed(context, '/password-reset-success');
+ } else {
+ // success가 false인 경우 에러 처리
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('비밀번호 변경에 실패했습니다.')));
+ }
+ } else if (response.statusCode == 500) {
+ // TODO: 500 에러 처리 구현 필요 - 미래에 구현 요청됨
+ developer.log('Password reset failed with 500 error');
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('서버 오류가 발생했습니다.')));
+ } else {
+ // 기타 에러 처리
+ developer.log(
+ 'Password reset failed with status: ${response.statusCode}',
+ );
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('비밀번호 변경 실패: ${response.statusCode}')),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ developer.log('Password reset error: $e');
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('네트워크 오류: $e')));
+ }
+ }
+
+ // 비밀번호 정책 에러 표시 위젯
+ Widget _buildPasswordPolicyErrors() {
+ if (_passwordPolicyErrors.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _passwordPolicyErrors.map((error) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ '• $error',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 24), // Top spacing
+ // Back Button
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: custom.CustomBackButton(onPressed: _handleBack),
+ ),
+
+ const SizedBox(height: 7),
+ // Page Title
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 30),
+ child: PageTitle(text: '비밀번호 변경', textAlign: TextAlign.left),
+ ),
+
+ const SizedBox(height: 48),
+ // New Password Input Field
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ LabeledInputField(
+ label: '변경할 비밀번호*',
+ placeholder: '비밀번호를 입력해주세요',
+ controller: _newPasswordController,
+ obscureText: true,
+ keyboardType: TextInputType.visiblePassword,
+ isError:
+ _isNewPasswordFieldError ||
+ _passwordPolicyErrors.isNotEmpty,
+ errorMessage: _newPasswordError,
+ onChanged: _onNewPasswordChanged,
+ ),
+ _buildPasswordPolicyErrors(),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24), // Space between password fields
+ // Confirm Password Input Field
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: LabeledInputField(
+ label: '변경할 비밀번호 확인*',
+ placeholder: '비밀번호를 다시 입력해주세요',
+ controller: _confirmPasswordController,
+ obscureText: true,
+ keyboardType: TextInputType.visiblePassword,
+ isError: _isConfirmPasswordFieldError,
+ errorMessage: _confirmPasswordError,
+ onChanged: _onConfirmPasswordChanged,
+ ),
+ ),
+
+ const SizedBox(
+ height: 140,
+ ), // Space between input fields and next button
+ // Next Button
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: NextButton(
+ text: _isLoading ? '처리 중...' : '다음',
+ onPressed: _isLoading ? null : _handleNext,
+ ),
+ ),
+
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/account/find_password_verification_page.dart b/frontend/lib/screens/account/find_password_verification_page.dart
new file mode 100644
index 0000000..c90ad26
--- /dev/null
+++ b/frontend/lib/screens/account/find_password_verification_page.dart
@@ -0,0 +1,253 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/verification_code_input.dart';
+import '../../widgets/next_button.dart';
+
+class FindPasswordVerificationPage extends StatefulWidget {
+ const FindPasswordVerificationPage({super.key});
+
+ @override
+ State createState() =>
+ _FindPasswordVerificationPageState();
+}
+
+class _FindPasswordVerificationPageState
+ extends State {
+ String _verificationCode = '';
+ bool _isLoading = false;
+ String? _userName;
+ String? _email;
+ String? _userId;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // arguments에서 데이터 추출
+ final args =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null) {
+ _userName = args['userName'] as String?;
+ _email = args['email'] as String?;
+ _userId = args['userId'] as String?;
+ }
+ }
+
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ void _handleNext() async {
+ if (_verificationCode.length != 6) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('6자리 인증번호를 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/verify/reset_password';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'email': _email ?? '',
+ 'code': _verificationCode,
+ };
+
+ developer.log('Sending password verification request: $requestData');
+
+ // API 호출
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+
+ if (responseData['success'] == true) {
+ // token 추출 및 저장
+ final token = responseData['data']?['token'];
+ if (token != null) {
+ developer.log('Token received: ${token.substring(0, 20)}...');
+ // 성공 시 비밀번호 재설정 페이지로 이동 (token 포함)
+ Navigator.pushNamed(
+ context,
+ '/reset-password',
+ arguments: {'userId': _userId, 'email': _email, 'token': token},
+ );
+ } else {
+ // token이 없는 경우
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('서버 응답에 토큰이 없습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } else {
+ // 인증 실패
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('인증번호가 올바르지 않습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } else {
+ // 서버 오류
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('서버 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } catch (e) {
+ developer.log('Error during password verification: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('네트워크 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ void _handleResendCode() {
+ // TODO: Implement resend verification code
+ developer.log('Resending verification code...');
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '비밀번호 찾기'),
+ ],
+ ),
+ const SizedBox(height: 20),
+ Text(
+ '${_userName ?? ''}님, 환영합니다.',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 24,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ const SizedBox(height: 7),
+ const Text(
+ '이메일로 보내드린 6자 코드를 입력해주세요.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 16,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ const SizedBox(height: 24),
+ VerificationCodeInput(
+ length: 6,
+ onChanged: (code) {
+ setState(() {
+ _verificationCode = code;
+ });
+ },
+ width: 320,
+ height: 50,
+ ),
+ const SizedBox(height: 20),
+ GestureDetector(
+ onTap: _handleResendCode,
+ child: const Text(
+ '인증번호를 받지 못했나요? 재전송',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ color: Color(0xFFADADAD),
+ ),
+ ),
+ ),
+ const SizedBox(height: 20),
+ NextButton(
+ text: _isLoading ? '처리 중...' : '다음',
+ onPressed: (_verificationCode.length == 6 && !_isLoading)
+ ? _handleNext
+ : null,
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/auth/login_page.dart b/frontend/lib/screens/auth/login_page.dart
new file mode 100644
index 0000000..fa4c433
--- /dev/null
+++ b/frontend/lib/screens/auth/login_page.dart
@@ -0,0 +1,327 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/app_logo.dart';
+import '../../widgets/input_field.dart';
+import '../../widgets/login_button.dart';
+import '../../widgets/sns_button.dart';
+import '../../widgets/links_section.dart';
+import '../../widgets/sns_divider.dart';
+
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State {
+ final TextEditingController _usernameController = TextEditingController();
+ final TextEditingController _passwordController = TextEditingController();
+
+ // 유효성 검사 상태
+ bool _isUsernameFieldError = false;
+ bool _isPasswordFieldError = false;
+ bool _isLoading = false;
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ void _clearErrors() {
+ setState(() {
+ _isUsernameFieldError = false;
+ _isPasswordFieldError = false;
+ });
+ }
+
+ bool _validateInputs() {
+ bool isValid = true;
+ _clearErrors();
+
+ // 아이디 유효성 검사
+ if (_usernameController.text.trim().isEmpty) {
+ setState(() {
+ _isUsernameFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 비밀번호 유효성 검사
+ if (_passwordController.text.trim().isEmpty) {
+ setState(() {
+ _isPasswordFieldError = true;
+ });
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ Future _handleLogin() async {
+ // 입력 값 검증
+ if (!_validateInputs()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/sign-in';
+
+ // 이미지 JSON 형식에 맞춰 요청 데이터 준비
+ final Map requestData = {
+ 'account_id': _usernameController.text.trim(),
+ 'password': _passwordController.text.trim(),
+ };
+
+ developer.log(
+ 'Login attempted with username: ${_usernameController.text.trim()}',
+ );
+ developer.log('Sending POST request to: $url');
+ developer.log('Request data: $requestData');
+
+ // HTTP POST 요청
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ setState(() {
+ _isLoading = false;
+ });
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ try {
+ final responseData = json.decode(response.body);
+
+ if (responseData['accessToken'] != null &&
+ responseData['refreshToken'] != null) {
+ // 토큰 저장 (향후 SecureStorage 사용 권장)
+ final accessToken = responseData['accessToken'];
+ final refreshToken = responseData['refreshToken'];
+ // final grantType = responseData['grantType'] ?? 'Bearer'; // 향후 사용 예정
+
+ developer.log('Login successful - tokens received');
+ developer.log('Access Token: ${accessToken.substring(0, 20)}...');
+ developer.log('Refresh Token: ${refreshToken.substring(0, 20)}...');
+
+ // 메인 네비게이션 화면으로 이동
+ if (mounted) {
+ Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
+ }
+ } else {
+ // 토큰이 없는 경우
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('로그인 응답에 토큰이 없습니다')));
+ }
+ }
+ } catch (e) {
+ developer.log('JSON parsing error: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('서버 응답을 처리할 수 없습니다')));
+ }
+ }
+ } else if (response.statusCode == 401) {
+ // 인증 실패
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('아이디 또는 비밀번호가 올바르지 않습니다')),
+ );
+ }
+ } else {
+ // 기타 오류
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('서버 오류: ${response.statusCode}')),
+ );
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ developer.log('Login error: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('네트워크 오류: $e')));
+ }
+ }
+ }
+
+ void _handleKakaoLogin() {
+ // Handle Kakao login logic here
+ developer.log('Kakao login attempted');
+ }
+
+ void _handleGoogleLogin() {
+ // Handle Google login logic here
+ developer.log('Google login attempted');
+ }
+
+ void _handleSignUp() {
+ Navigator.pushNamed(context, '/signup');
+ }
+
+ void _handleFindID() {
+ Navigator.pushNamed(context, '/find-id');
+ }
+
+ void _handleFindPW() {
+ Navigator.pushNamed(context, '/find-password');
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Center(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(height: 60), // Top spacing
+ // App Logo
+ const AppLogo(),
+
+ const SizedBox(
+ height: 80,
+ ), // Space between logo and input fields
+ // Input Fields
+ Container(
+ width: MediaQuery.of(context).size.width * 0.9,
+ constraints: const BoxConstraints(
+ maxWidth: 400,
+ minWidth: 300,
+ ),
+ child: Column(
+ children: [
+ // Username Input
+ InputField(
+ placeholder: '아이디',
+ controller: _usernameController,
+ keyboardType: TextInputType.text,
+ isError: _isUsernameFieldError,
+ ),
+
+ const SizedBox(height: 20),
+
+ // Password Input
+ InputField(
+ placeholder: '비밀번호',
+ controller: _passwordController,
+ obscureText: true,
+ isError: _isPasswordFieldError,
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(
+ height: 35,
+ ), // Space between input fields and login button
+ // Login Button
+ LoginButton(
+ text: _isLoading ? '로그인 중...' : '로그인',
+ onPressed: _isLoading ? null : _handleLogin,
+ ),
+
+ const SizedBox(
+ height: 40,
+ ), // Space between login button and links
+ // Links Section
+ LinksSection(
+ onSignUp: _handleSignUp,
+ onFindID: _handleFindID,
+ onFindPW: _handleFindPW,
+ ),
+
+ const SizedBox(
+ height: 40,
+ ), // Space between links and SNS divider
+ // SNS Divider
+ const SNSDivider(),
+
+ const SizedBox(
+ height: 30,
+ ), // Space between divider and SNS buttons
+ // SNS Buttons
+ Container(
+ constraints: const BoxConstraints(
+ maxWidth: 200,
+ minWidth: 150,
+ ),
+ height: 50,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Kakao Button
+ SNSButton(
+ provider: SNSProvider.kakao,
+ onPressed: _handleKakaoLogin,
+ ),
+
+ const SizedBox(width: 20), // 버튼 간격
+ // Google Button
+ SNSButton(
+ provider: SNSProvider.google,
+ onPressed: _handleGoogleLogin,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/auth/password_reset_form_page.dart b/frontend/lib/screens/auth/password_reset_form_page.dart
new file mode 100644
index 0000000..0316b9a
--- /dev/null
+++ b/frontend/lib/screens/auth/password_reset_form_page.dart
@@ -0,0 +1,399 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/next_button.dart';
+import '../../widgets/input_field.dart';
+
+class PasswordResetFormPage extends StatefulWidget {
+ const PasswordResetFormPage({super.key});
+
+ @override
+ State createState() => _PasswordResetFormPageState();
+}
+
+class _PasswordResetFormPageState extends State {
+ final TextEditingController _newPasswordController = TextEditingController();
+ final TextEditingController _confirmPasswordController =
+ TextEditingController();
+ bool _isLoading = false;
+ String? _token;
+
+ // 비밀번호 정책 검사 결과
+ List _passwordPolicyErrors = [];
+ String? _confirmPasswordError;
+ bool _isConfirmPasswordFieldError = false;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // arguments에서 token 추출
+ final args =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null) {
+ _token = args['token'] as String?;
+ }
+ }
+
+ @override
+ void dispose() {
+ _newPasswordController.dispose();
+ _confirmPasswordController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ // 로그인 페이지로 이동
+ Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
+ }
+
+ // 비밀번호 정책 검사
+ List _validatePasswordPolicy(String password) {
+ List errors = [];
+
+ if (password.length < 8) {
+ errors.add('8자 이상이어야 합니다');
+ }
+ if (!RegExp(r'[A-Z]').hasMatch(password)) {
+ errors.add('대문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[a-z]').hasMatch(password)) {
+ errors.add('소문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[0-9]').hasMatch(password)) {
+ errors.add('숫자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
+ errors.add('특수문자를 포함해야 합니다');
+ }
+
+ return errors;
+ }
+
+ // 실시간 비밀번호 유효성 검사
+ void _onPasswordChanged(String value) {
+ setState(() {
+ _passwordPolicyErrors = _validatePasswordPolicy(value);
+ });
+
+ // 비밀번호 확인 필드도 실시간으로 검사
+ if (_confirmPasswordController.text.isNotEmpty) {
+ _onConfirmPasswordChanged(_confirmPasswordController.text);
+ }
+ }
+
+ // 실시간 비밀번호 확인 검사
+ void _onConfirmPasswordChanged(String value) {
+ setState(() {
+ if (value.isNotEmpty && _newPasswordController.text != value) {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ } else {
+ _confirmPasswordError = null;
+ _isConfirmPasswordFieldError = false;
+ }
+ });
+ }
+
+ // 비밀번호 정책 에러 표시 위젯
+ Widget _buildPasswordPolicyErrors() {
+ if (_passwordPolicyErrors.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _passwordPolicyErrors.map((error) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ '• $error',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ Future _handleResetPassword() async {
+ // 입력값 검증
+ if (_newPasswordController.text.trim().isEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('새 비밀번호를 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ // 비밀번호 정책 검사
+ List policyErrors = _validatePasswordPolicy(
+ _newPasswordController.text,
+ );
+ if (policyErrors.isNotEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 정책을 만족해야 합니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_confirmPasswordController.text.trim().isEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 확인을 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_newPasswordController.text != _confirmPasswordController.text) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호가 일치하지 않습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_token == null) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('인증 토큰이 없습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회
+ HttpOverrides.global = MyHttpOverrides();
+
+ const String serverIp = '3.34.214.133';
+ const String url = 'https://$serverIp/users/reset-password';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'token': _token!,
+ 'new_password': _newPasswordController.text.trim(),
+ };
+
+ developer.log('Sending password reset request: $requestData');
+
+ // API 호출
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+
+ if (responseData['success'] == true) {
+ // 성공 시 성공 페이지로 이동
+ if (mounted) {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/password-reset-success',
+ (route) => false,
+ );
+ }
+ } else {
+ // 실패
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 변경에 실패했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } else {
+ // 서버 오류
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('서버 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } catch (e) {
+ developer.log('Error during password reset: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('네트워크 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 24), // Top spacing
+ // Back Button
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: custom.CustomBackButton(onPressed: _handleBack),
+ ),
+
+ const SizedBox(height: 7),
+ // Page Title
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 30),
+ child: PageTitle(text: '비밀번호 변경', textAlign: TextAlign.left),
+ ),
+
+ const SizedBox(height: 48),
+ // Instruction Message
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 30),
+ child: Text(
+ '새 비밀번호를 입력해주세요.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 24,
+ height: 1.193359375,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 40),
+ // New Password Input
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InputField(
+ placeholder: '새 비밀번호',
+ controller: _newPasswordController,
+ obscureText: true,
+ onChanged: _onPasswordChanged,
+ isError: _passwordPolicyErrors.isNotEmpty,
+ ),
+ _buildPasswordPolicyErrors(),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 20), // Space between input fields
+ // Confirm Password Input
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InputField(
+ placeholder: '비밀번호 확인',
+ controller: _confirmPasswordController,
+ obscureText: true,
+ onChanged: _onConfirmPasswordChanged,
+ isError: _isConfirmPasswordFieldError,
+ ),
+ if (_confirmPasswordError != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ _confirmPasswordError!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 40),
+ // Reset Password Button
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30),
+ child: NextButton(
+ text: _isLoading ? '처리 중...' : '비밀번호 변경',
+ onPressed: _isLoading ? null : _handleResetPassword,
+ ),
+ ),
+
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/auth/password_reset_success_page.dart b/frontend/lib/screens/auth/password_reset_success_page.dart
new file mode 100644
index 0000000..82849ee
--- /dev/null
+++ b/frontend/lib/screens/auth/password_reset_success_page.dart
@@ -0,0 +1,91 @@
+import 'package:flutter/material.dart';
+import '../../widgets/login_button.dart';
+
+class PasswordResetSuccessPage extends StatelessWidget {
+ const PasswordResetSuccessPage({super.key});
+
+ void _handleLogin(BuildContext context) {
+ // 로그인 페이지로 이동
+ Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final screenHeight =
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom;
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(minHeight: screenHeight),
+ child: IntrinsicHeight(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(height: 120), // Top spacing
+ // Success Icon
+ Container(
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ color: const Color(0xFF6B4EFF),
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.check,
+ color: Colors.white,
+ size: 60,
+ ),
+ ),
+
+ const SizedBox(height: 40),
+ // Success Message
+ const Text(
+ '비밀번호 변경 완료',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 28,
+ color: Color(0xFF2C2C2C),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+ // Sub Message
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 40),
+ child: Text(
+ '새로운 비밀번호로\n로그인해주세요.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 16,
+ height: 1.5,
+ color: Color(0xFF7C7C7C),
+ ),
+ ),
+ ),
+
+ const Expanded(child: SizedBox()), // Push button to bottom
+ // Login Button
+ Padding(
+ padding: const EdgeInsets.only(bottom: 40),
+ child: LoginButton(
+ text: '로그인하기',
+ onPressed: () => _handleLogin(context),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/auth/reset_password_page.dart b/frontend/lib/screens/auth/reset_password_page.dart
new file mode 100644
index 0000000..88d7171
--- /dev/null
+++ b/frontend/lib/screens/auth/reset_password_page.dart
@@ -0,0 +1,388 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/input_field.dart';
+import '../../widgets/next_button.dart';
+
+class ResetPasswordPage extends StatefulWidget {
+ const ResetPasswordPage({super.key});
+
+ @override
+ State createState() => _ResetPasswordPageState();
+}
+
+class _ResetPasswordPageState extends State {
+ final TextEditingController _newPasswordController = TextEditingController();
+ final TextEditingController _confirmPasswordController =
+ TextEditingController();
+ bool _isLoading = false;
+ String? _token;
+
+ // 비밀번호 정책 검사 결과
+ List _passwordPolicyErrors = [];
+ String? _confirmPasswordError;
+ bool _isConfirmPasswordFieldError = false;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // arguments에서 데이터 추출
+ final args =
+ ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null) {
+ _token = args['token'] as String?;
+ developer.log(
+ 'Received token: ${_token != null ? "${_token!.substring(0, 20)}..." : "null"}',
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ _newPasswordController.dispose();
+ _confirmPasswordController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ // 비밀번호 정책 검사
+ List _validatePasswordPolicy(String password) {
+ List errors = [];
+
+ if (password.length < 8) {
+ errors.add('8자 이상이어야 합니다');
+ }
+ if (!RegExp(r'[A-Z]').hasMatch(password)) {
+ errors.add('대문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[a-z]').hasMatch(password)) {
+ errors.add('소문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[0-9]').hasMatch(password)) {
+ errors.add('숫자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
+ errors.add('특수문자를 포함해야 합니다');
+ }
+
+ return errors;
+ }
+
+ // 실시간 비밀번호 유효성 검사
+ void _onPasswordChanged(String value) {
+ setState(() {
+ _passwordPolicyErrors = _validatePasswordPolicy(value);
+ });
+
+ // 비밀번호 확인 필드도 실시간으로 검사
+ if (_confirmPasswordController.text.isNotEmpty) {
+ _onConfirmPasswordChanged(_confirmPasswordController.text);
+ }
+ }
+
+ // 실시간 비밀번호 확인 검사
+ void _onConfirmPasswordChanged(String value) {
+ setState(() {
+ if (value.isNotEmpty && _newPasswordController.text != value) {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ } else {
+ _confirmPasswordError = null;
+ _isConfirmPasswordFieldError = false;
+ }
+ });
+ }
+
+ // 비밀번호 정책 에러 표시 위젯
+ Widget _buildPasswordPolicyErrors() {
+ if (_passwordPolicyErrors.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _passwordPolicyErrors.map((error) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ '• $error',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ void _handleResetPassword() async {
+ // 기본 유효성 검사
+ if (_newPasswordController.text.trim().isEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호를 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ // 비밀번호 정책 검사
+ List policyErrors = _validatePasswordPolicy(
+ _newPasswordController.text,
+ );
+ if (policyErrors.isNotEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 정책을 만족해야 합니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_confirmPasswordController.text.trim().isEmpty) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 확인을 입력해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_newPasswordController.text != _confirmPasswordController.text) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호가 일치하지 않습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ if (_token == null) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('인증 토큰이 없습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = MyHttpOverrides();
+
+ // 서버 IP 설정 (필요에 따라 변경)
+ const String serverIp = '3.34.214.133'; // 실제 서버 IP로 변경해주세요
+ const String url = 'https://$serverIp/users/reset-password';
+
+ // 요청 데이터 준비
+ final Map requestData = {
+ 'token': _token!,
+ 'new_password': _newPasswordController.text,
+ };
+
+ developer.log('Sending password change request: $requestData');
+
+ // API 호출
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+
+ if (responseData['success'] == true) {
+ // 성공 시 성공 페이지로 이동
+ if (mounted) {
+ Navigator.of(context).pushNamedAndRemoveUntil(
+ '/password-reset-success',
+ (route) => false,
+ );
+ }
+ } else {
+ // 실패
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('비밀번호 변경에 실패했습니다.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } else {
+ // 서버 오류
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('서버 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ }
+ } catch (e) {
+ developer.log('Error during password change: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('네트워크 오류가 발생했습니다. 다시 시도해주세요.'),
+ backgroundColor: Color(0xFFFF4258),
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '비밀번호 재설정'),
+ ],
+ ),
+ const SizedBox(height: 40),
+ const Text(
+ '새로운 비밀번호를 설정해주세요.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 18,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ '안전한 비밀번호로 계정을 보호하세요.',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ const SizedBox(height: 40),
+ // New Password Input
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InputField(
+ placeholder: '새 비밀번호',
+ controller: _newPasswordController,
+ obscureText: true,
+ onChanged: _onPasswordChanged,
+ isError: _passwordPolicyErrors.isNotEmpty,
+ ),
+ _buildPasswordPolicyErrors(),
+ ],
+ ),
+ const SizedBox(height: 20),
+ // Confirm Password Input
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InputField(
+ placeholder: '비밀번호 확인',
+ controller: _confirmPasswordController,
+ obscureText: true,
+ onChanged: _onConfirmPasswordChanged,
+ isError: _isConfirmPasswordFieldError,
+ ),
+ if (_confirmPasswordError != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ _confirmPasswordError!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ],
+ ],
+ ),
+ const SizedBox(height: 40),
+ NextButton(
+ text: _isLoading ? '처리 중...' : '확인',
+ onPressed: _isLoading ? null : _handleResetPassword,
+ ),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스
+class MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/auth/signup_page.dart b/frontend/lib/screens/auth/signup_page.dart
new file mode 100644
index 0000000..7e043aa
--- /dev/null
+++ b/frontend/lib/screens/auth/signup_page.dart
@@ -0,0 +1,1088 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/labeled_input_field.dart';
+import '../../widgets/next_button.dart';
+
+class SignUpPage extends StatefulWidget {
+ const SignUpPage({super.key});
+
+ @override
+ State createState() => _SignUpPageState();
+}
+
+class _SignUpPageState extends State {
+ final TextEditingController _nameController = TextEditingController();
+ final TextEditingController _idController = TextEditingController();
+ final TextEditingController _emailController = TextEditingController();
+ final TextEditingController _emailCodeController = TextEditingController();
+ final TextEditingController _phoneController = TextEditingController();
+ final TextEditingController _passwordController = TextEditingController();
+ final TextEditingController _confirmPasswordController =
+ TextEditingController();
+
+ // 유효성 검사 상태
+ String? _idError;
+ String? _emailError;
+ String? _emailCodeError;
+ String? _phoneError;
+ String? _passwordError;
+ String? _confirmPasswordError;
+ bool _isIdFieldError = false;
+ bool _isEmailFieldError = false;
+ bool _isEmailCodeFieldError = false;
+ bool _isPhoneFieldError = false;
+ bool _isPasswordFieldError = false;
+ bool _isConfirmPasswordFieldError = false;
+
+ // API 호출 상태
+ bool _isCheckingIdDuplicate = false;
+ bool _isSendingEmailCode = false;
+ bool _isIdDuplicateChecked = false;
+ bool _isEmailVerified = false;
+ bool _isEmailCodeEnabled = false;
+ bool _isVerifyingEmailCode = false;
+
+ // 비밀번호 정책 검사 결과
+ List _passwordPolicyErrors = [];
+ String? _emailCodeSuccessMessage;
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ _idController.dispose();
+ _emailController.dispose();
+ _emailCodeController.dispose();
+ _phoneController.dispose();
+ _passwordController.dispose();
+ _confirmPasswordController.dispose();
+ super.dispose();
+ }
+
+ void _handleBack() {
+ Navigator.pop(context);
+ }
+
+ // 이메일 형식 검증
+ bool _isValidEmail(String email) {
+ final emailRegex = RegExp(r'^.+@.+$');
+ return emailRegex.hasMatch(email);
+ }
+
+ // 비밀번호 정책 검사
+ List _validatePasswordPolicy(String password) {
+ List errors = [];
+
+ if (password.length < 8) {
+ errors.add('8자 이상이어야 합니다');
+ }
+ if (!RegExp(r'[A-Z]').hasMatch(password)) {
+ errors.add('대문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[a-z]').hasMatch(password)) {
+ errors.add('소문자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[0-9]').hasMatch(password)) {
+ errors.add('숫자를 포함해야 합니다');
+ }
+ if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
+ errors.add('특수문자를 포함해야 합니다');
+ }
+
+ return errors;
+ }
+
+ // 실시간 비밀번호 유효성 검사
+ void _onPasswordChanged(String value) {
+ setState(() {
+ _passwordPolicyErrors = _validatePasswordPolicy(value);
+ _passwordError = null;
+ _isPasswordFieldError = false;
+ });
+
+ // 비밀번호 확인 필드도 실시간으로 검사
+ if (_confirmPasswordController.text.isNotEmpty) {
+ _onConfirmPasswordChanged(_confirmPasswordController.text);
+ }
+ }
+
+ // 실시간 비밀번호 확인 검사
+ void _onConfirmPasswordChanged(String value) {
+ setState(() {
+ if (value.isNotEmpty && _passwordController.text != value) {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ } else {
+ _confirmPasswordError = null;
+ _isConfirmPasswordFieldError = false;
+ }
+ });
+ }
+
+ // 실시간 휴대폰 번호 검사
+ void _onPhoneChanged(String value) {
+ setState(() {
+ if (value.isNotEmpty && !RegExp(r'^[0-9]+$').hasMatch(value)) {
+ _phoneError = '숫자만 입력해주세요';
+ _isPhoneFieldError = true;
+ } else {
+ _phoneError = null;
+ _isPhoneFieldError = false;
+ }
+ });
+ }
+
+ void _clearErrors() {
+ setState(() {
+ _idError = null;
+ _emailError = null;
+ _emailCodeError = null;
+ _phoneError = null;
+ _passwordError = null;
+ _confirmPasswordError = null;
+ _isIdFieldError = false;
+ _isEmailFieldError = false;
+ _isEmailCodeFieldError = false;
+ _isPhoneFieldError = false;
+ _isPasswordFieldError = false;
+ _isConfirmPasswordFieldError = false;
+ _passwordPolicyErrors = [];
+ });
+ }
+
+ bool _validateInputs() {
+ bool isValid = true;
+ _clearErrors();
+
+ // 이름 유효성 검사
+ if (_nameController.text.trim().isEmpty) {
+ // 이름은 별도 에러 메시지 없이 기본적으로 처리
+ isValid = false;
+ }
+
+ // 아이디 유효성 검사
+ if (_idController.text.trim().isEmpty) {
+ setState(() {
+ _idError = '아이디를 입력해주세요';
+ _isIdFieldError = true;
+ });
+ isValid = false;
+ } else if (!_isIdDuplicateChecked) {
+ setState(() {
+ _idError = '아이디 중복 확인을 해주세요';
+ _isIdFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 이메일 유효성 검사
+ if (_emailController.text.trim().isEmpty) {
+ setState(() {
+ _emailError = '이메일을 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ } else if (!_isValidEmail(_emailController.text.trim())) {
+ setState(() {
+ _emailError = '올바른 이메일 주소를 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 이메일 인증 유효성 검사
+ if (!_isEmailVerified) {
+ setState(() {
+ _emailCodeError = '이메일 인증을 완료해주세요';
+ _isEmailCodeFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 휴대폰 번호 유효성 검사
+ if (_phoneController.text.trim().isEmpty) {
+ setState(() {
+ _phoneError = '휴대폰 번호를 입력해주세요';
+ _isPhoneFieldError = true;
+ });
+ isValid = false;
+ } else if (!RegExp(r'^[0-9]+$').hasMatch(_phoneController.text.trim())) {
+ setState(() {
+ _phoneError = '숫자만 입력해주세요';
+ _isPhoneFieldError = true;
+ });
+ isValid = false;
+ }
+
+ // 비밀번호 유효성 검사
+ if (_passwordController.text.trim().isEmpty) {
+ setState(() {
+ _passwordError = '비밀번호를 입력해주세요';
+ _isPasswordFieldError = true;
+ });
+ isValid = false;
+ } else {
+ List policyErrors = _validatePasswordPolicy(
+ _passwordController.text,
+ );
+ if (policyErrors.isNotEmpty) {
+ setState(() {
+ _passwordPolicyErrors = policyErrors;
+ });
+ isValid = false;
+ }
+ }
+
+ // 비밀번호 확인 유효성 검사
+ if (_confirmPasswordController.text.trim().isEmpty) {
+ setState(() {
+ _confirmPasswordError = '비밀번호 확인을 입력해주세요';
+ _isConfirmPasswordFieldError = true;
+ });
+ isValid = false;
+ } else if (_passwordController.text != _confirmPasswordController.text) {
+ setState(() {
+ _confirmPasswordError = '입력된 비밀번호가 다릅니다';
+ _isConfirmPasswordFieldError = true;
+ });
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ // 아이디 중복 검사
+ Future _checkIdDuplicate() async {
+ if (_idController.text.trim().isEmpty) {
+ setState(() {
+ _idError = '아이디를 입력해주세요';
+ _isIdFieldError = true;
+ });
+ return;
+ }
+
+ setState(() {
+ _isCheckingIdDuplicate = true;
+ _idError = null;
+ _isIdFieldError = false;
+ });
+
+ try {
+ HttpOverrides.global = _MyHttpOverrides();
+
+ const String serverIp = '3.34.214.133';
+ final String accountId = _idController.text.trim();
+ final String url =
+ 'https://$serverIp/users/check-accountId?accountId=$accountId';
+
+ developer.log('GET $url');
+ developer.log('Checking account ID: $accountId');
+
+ // HTTP GET 요청
+ final response = await http.get(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ setState(() {
+ _isCheckingIdDuplicate = false;
+ });
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ final responseData = json.decode(response.body);
+
+ if (responseData['success'] == true) {
+ // 성공: 아이디 사용 가능
+ setState(() {
+ _isIdDuplicateChecked = true;
+ _idError = null;
+ _isIdFieldError = false;
+ });
+ developer.log('Account ID is available: $accountId');
+ } else {
+ // 실패: 중복된 아이디
+ setState(() {
+ _isIdDuplicateChecked = false;
+ _idError = '중복된 아이디입니다.';
+ _isIdFieldError = true;
+ });
+ developer.log('Account ID is duplicated: $accountId');
+ }
+ } else {
+ // 서버 오류
+ setState(() {
+ _isIdDuplicateChecked = false;
+ _idError = '중복 확인 중 오류가 발생했습니다';
+ _isIdFieldError = true;
+ });
+ developer.log('Server error: ${response.statusCode}');
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isCheckingIdDuplicate = false;
+ _idError = '중복 확인 중 오류가 발생했습니다';
+ _isIdFieldError = true;
+ _isIdDuplicateChecked = false;
+ });
+ }
+ developer.log('Check ID duplicate error: $e');
+ }
+ }
+
+ // 이메일 인증번호 발송
+ Future _sendEmailCode() async {
+ if (_emailController.text.trim().isEmpty) {
+ setState(() {
+ _emailError = '이메일을 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ return;
+ }
+
+ if (!_isValidEmail(_emailController.text.trim())) {
+ setState(() {
+ _emailError = '올바른 이메일 주소를 입력해주세요';
+ _isEmailFieldError = true;
+ });
+ return;
+ }
+
+ setState(() {
+ _isSendingEmailCode = true;
+ _emailError = null;
+ _isEmailFieldError = false;
+ _isEmailCodeEnabled = false;
+ _emailCodeSuccessMessage = null;
+ });
+
+ try {
+ HttpOverrides.global = _MyHttpOverrides();
+
+ const String serverIp = '3.34.214.133';
+ final String url = 'https://$serverIp/send-code/sign_up';
+ final Map requestData = {
+ 'email': _emailController.text.trim(),
+ };
+
+ developer.log('POST $url');
+ developer.log('Request: $requestData');
+
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (mounted) {
+ setState(() {
+ _isSendingEmailCode = false;
+ });
+ }
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final data = json.decode(response.body);
+ if (data['success'] == true) {
+ setState(() {
+ _isEmailCodeEnabled = true;
+ _isEmailVerified = false;
+ _emailCodeController.clear();
+ _emailError = null;
+ _isEmailFieldError = false;
+ });
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('인증번호가 발송되었습니다.')));
+ } else {
+ setState(() {
+ _isEmailCodeEnabled = false;
+ _emailError = '올바른 이메일인지 확인해주세요.';
+ _isEmailFieldError = true;
+ });
+ }
+ } else {
+ setState(() {
+ _isEmailCodeEnabled = false;
+ _emailError = '올바른 이메일인지 확인해주세요.';
+ _isEmailFieldError = true;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isSendingEmailCode = false;
+ _isEmailCodeEnabled = false;
+ _emailError = '올바른 이메일인지 확인해주세요.';
+ _isEmailFieldError = true;
+ });
+ }
+ developer.log('Send email code error: $e');
+ }
+ }
+
+ // 이메일 인증번호 확인
+ Future _verifyEmailCode() async {
+ if (_emailCodeController.text.trim().isEmpty) {
+ setState(() {
+ _emailCodeError = '인증번호를 입력해주세요';
+ _isEmailCodeFieldError = true;
+ });
+ return;
+ }
+
+ setState(() {
+ _emailCodeError = null;
+ _isEmailCodeFieldError = false;
+ });
+
+ try {
+ HttpOverrides.global = _MyHttpOverrides();
+
+ const String serverIp = '3.34.214.133';
+ final String url = 'https://$serverIp/verify/sign_up';
+
+ setState(() {
+ _isVerifyingEmailCode = true;
+ _emailCodeSuccessMessage = null;
+ });
+
+ final Map requestData = {
+ 'email': _emailController.text.trim(),
+ 'code': _emailCodeController.text.trim(),
+ };
+
+ developer.log('POST $url');
+ developer.log('Request data: $requestData');
+ developer.log('Email: ${_emailController.text.trim()}');
+ developer.log('Code: ${_emailCodeController.text.trim()}');
+ developer.log('Request body: ${json.encode(requestData)}');
+
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (response.statusCode != 200) {
+ developer.log('ERROR: Non-200 status code received');
+ developer.log('Response headers: ${response.headers}');
+ }
+
+ if (mounted) {
+ setState(() {
+ _isVerifyingEmailCode = false;
+ });
+ }
+ if (!mounted) return;
+
+ if (response.statusCode == 200) {
+ final data = json.decode(response.body);
+ if (data['success'] == true) {
+ setState(() {
+ _isEmailVerified = true;
+ _isEmailCodeEnabled = false; // 입력 폼 비활성화 (값 유지)
+ _emailCodeError = null;
+ _isEmailCodeFieldError = false;
+ _emailCodeSuccessMessage = '인증이 완료되었습니다.';
+ });
+ } else {
+ final errorMessage = data['error'] ?? '인증번호가 올바르지 않습니다';
+ setState(() {
+ _isEmailVerified = false;
+ _emailCodeError = errorMessage;
+ _isEmailCodeFieldError = true;
+ _emailCodeSuccessMessage = null;
+ });
+ developer.log('Verification failed: $errorMessage');
+ }
+ } else if (response.statusCode == 500) {
+ // 서버 에러
+ setState(() {
+ _isEmailVerified = false;
+ _emailCodeError = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
+ _isEmailCodeFieldError = true;
+ _emailCodeSuccessMessage = null;
+ });
+ developer.log('Server error 500: ${response.body}');
+ } else {
+ setState(() {
+ _isEmailVerified = false;
+ _emailCodeError = '인증번호 확인에 실패했습니다. (에러 코드: ${response.statusCode})';
+ _isEmailCodeFieldError = true;
+ _emailCodeSuccessMessage = null;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isVerifyingEmailCode = false;
+ _isEmailVerified = false;
+ _emailCodeError = '인증번호 확인 중 오류가 발생했습니다';
+ _isEmailCodeFieldError = true;
+ _emailCodeSuccessMessage = null;
+ });
+ }
+ developer.log('Verify email code error: $e');
+ }
+ }
+
+ void _handleSignUp() {
+ if (!_validateInputs()) {
+ return;
+ }
+
+ // 모든 유효성 검사 통과 시 권한 동의 페이지로 이동
+ Navigator.pushNamed(
+ context,
+ '/signup-terms',
+ arguments: {
+ 'name': _nameController.text.trim(),
+ 'id': _idController.text.trim(),
+ 'email': _emailController.text.trim(),
+ 'phone': _phoneController.text.trim(),
+ 'password': _passwordController.text.trim(),
+ },
+ );
+ }
+
+ // 비밀번호 정책 에러 표시 위젯
+ Widget _buildPasswordPolicyErrors() {
+ if (_passwordPolicyErrors.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _passwordPolicyErrors.map((error) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ '• $error',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 30.0),
+ child: Column(
+ children: [
+ const SizedBox(height: 20),
+ Row(
+ children: [
+ custom.CustomBackButton(onPressed: _handleBack),
+ const SizedBox(width: 20),
+ const PageTitle(text: '회원가입'),
+ ],
+ ),
+ const SizedBox(height: 20),
+ LabeledInputField(
+ label: '이름*',
+ placeholder: '이름을 입력해주세요',
+ controller: _nameController,
+ keyboardType: TextInputType.text,
+ ),
+ const SizedBox(height: 24),
+ // 아이디 입력 필드와 중복 확인 버튼
+ Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ flex: 7,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Label
+ const Text(
+ '아이디*',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+
+ const SizedBox(height: 8),
+
+ // Input Field
+ Container(
+ height: 50,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF3F3F3),
+ borderRadius: BorderRadius.circular(5),
+ border: _isIdFieldError
+ ? Border.all(
+ color: const Color(0xFFFF4258),
+ width: 1,
+ )
+ : null,
+ ),
+ child: TextField(
+ controller: _idController,
+ keyboardType: TextInputType.text,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: _isIdFieldError
+ ? const Color(0xFFFF4258)
+ : const Color(0xFFA0A0A0),
+ ),
+ decoration: InputDecoration(
+ hintText: '아이디를 입력해주세요',
+ hintStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ border: InputBorder.none,
+ contentPadding:
+ const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+ onChanged: (value) {
+ // 아이디가 변경되면 중복 확인 상태 초기화
+ if (_isIdDuplicateChecked) {
+ setState(() {
+ _isIdDuplicateChecked = false;
+ });
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(width: 12),
+
+ Expanded(
+ flex: 3,
+ child: SizedBox(
+ height: 50,
+ child: ElevatedButton(
+ onPressed: _isCheckingIdDuplicate
+ ? null
+ : _checkIdDuplicate,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _isIdDuplicateChecked
+ ? const Color(0xFF10B981)
+ : const Color(0xFF6366F1),
+ foregroundColor: Colors.white,
+ elevation: 0,
+ padding: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: Text(
+ _isCheckingIdDuplicate
+ ? '확인 중...'
+ : _isIdDuplicateChecked
+ ? '사용 가능'
+ : '중복 확인',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Error Message (Row 밖으로 이동)
+ if (_isIdFieldError && _idError != null) ...[
+ const SizedBox(height: 8),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ _idError!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+ const SizedBox(height: 24),
+ // 이메일 입력 필드와 인증번호 발송 버튼
+ Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ flex: 7,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Label
+ const Text(
+ '이메일*',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+
+ const SizedBox(height: 8),
+
+ // Input Field
+ Container(
+ height: 50,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF3F3F3),
+ borderRadius: BorderRadius.circular(5),
+ border: _isEmailFieldError
+ ? Border.all(
+ color: const Color(0xFFFF4258),
+ width: 1,
+ )
+ : null,
+ ),
+ child: TextField(
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: _isEmailFieldError
+ ? const Color(0xFFFF4258)
+ : const Color(0xFFA0A0A0),
+ ),
+ decoration: InputDecoration(
+ hintText: '이메일 주소를 입력해주세요',
+ hintStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ border: InputBorder.none,
+ contentPadding:
+ const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+ onChanged: (value) {
+ // 이메일이 변경되면 인증 상태 초기화
+ if (_isEmailVerified) {
+ setState(() {
+ _isEmailVerified = false;
+ _emailCodeController.clear();
+ });
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(width: 12),
+
+ Expanded(
+ flex: 3,
+ child: SizedBox(
+ height: 50,
+ child: ElevatedButton(
+ onPressed: _isSendingEmailCode
+ ? null
+ : _sendEmailCode,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF6366F1),
+ foregroundColor: Colors.white,
+ elevation: 0,
+ padding: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: Text(
+ _isSendingEmailCode ? '발송 중...' : '인증번호 발송',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Error Message (Row 밖으로 이동)
+ if (_isEmailFieldError && _emailError != null) ...[
+ const SizedBox(height: 8),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ _emailError!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+ const SizedBox(height: 16),
+ // 인증번호 입력 필드 + 확인 버튼
+ Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ flex: 7,
+ child: Container(
+ height: 50,
+ decoration: BoxDecoration(
+ border: _isEmailCodeFieldError
+ ? Border.all(
+ color: const Color(0xFFFF4258),
+ width: 1,
+ )
+ : Border.all(
+ color: const Color(0xFFE5E7EB),
+ width: 1,
+ ),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TextField(
+ controller: _emailCodeController,
+ enabled:
+ _isEmailCodeEnabled || _isEmailVerified,
+ keyboardType: TextInputType.number,
+ decoration: const InputDecoration(
+ hintText: '인증번호를 입력해주세요',
+ hintStyle: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF9CA3AF),
+ ),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 14,
+ ),
+ ),
+ onSubmitted: (value) {
+ if (value.isNotEmpty &&
+ (_isEmailCodeEnabled ||
+ _isEmailVerified)) {
+ _verifyEmailCode();
+ }
+ },
+ ),
+ ),
+ ),
+
+ const SizedBox(width: 12),
+
+ Expanded(
+ flex: 3,
+ child: SizedBox(
+ height: 50,
+ child: ElevatedButton(
+ onPressed:
+ (!_isEmailCodeEnabled ||
+ _isVerifyingEmailCode)
+ ? null
+ : _verifyEmailCode,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF6366F1),
+ foregroundColor: Colors.white,
+ elevation: 0,
+ padding: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: Text(
+ _isVerifyingEmailCode ? '확인 중...' : '인증번호 확인',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ // Error Message (Row 밖으로 이동)
+ if (_isEmailCodeFieldError &&
+ _emailCodeError != null) ...[
+ const SizedBox(height: 8),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ _emailCodeError!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ),
+ ],
+
+ // Success Message
+ if (_emailCodeSuccessMessage != null) ...[
+ const SizedBox(height: 8),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ _emailCodeSuccessMessage!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFF10B981),
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+
+ const SizedBox(height: 24),
+ LabeledInputField(
+ label: '휴대전화*',
+ placeholder: '전화번호를 입력해주세요',
+ controller: _phoneController,
+ keyboardType: TextInputType.phone,
+ isError: _isPhoneFieldError,
+ errorMessage: _phoneError,
+ onChanged: _onPhoneChanged,
+ ),
+ const SizedBox(height: 24),
+ // 비밀번호 입력 필드와 정책 에러 표시
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ LabeledInputField(
+ label: '비밀번호*',
+ placeholder: '비밀번호를 입력해주세요',
+ controller: _passwordController,
+ keyboardType: TextInputType.visiblePassword,
+ obscureText: true,
+ isError:
+ _isPasswordFieldError ||
+ _passwordPolicyErrors.isNotEmpty,
+ errorMessage: _passwordError,
+ onChanged: _onPasswordChanged,
+ ),
+ _buildPasswordPolicyErrors(),
+ ],
+ ),
+ const SizedBox(height: 24),
+ LabeledInputField(
+ label: '비밀번호 확인*',
+ placeholder: '비밀번호를 다시 입력해주세요',
+ controller: _confirmPasswordController,
+ keyboardType: TextInputType.visiblePassword,
+ obscureText: true,
+ isError: _isConfirmPasswordFieldError,
+ errorMessage: _confirmPasswordError,
+ onChanged: _onConfirmPasswordChanged,
+ ),
+ const SizedBox(height: 20),
+ NextButton(text: '다음', onPressed: _handleSignUp),
+ const SizedBox(height: 60), // Bottom spacing
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스 (프로덕션에서는 제거)
+class _MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/auth/signup_success_page.dart b/frontend/lib/screens/auth/signup_success_page.dart
new file mode 100644
index 0000000..a59010d
--- /dev/null
+++ b/frontend/lib/screens/auth/signup_success_page.dart
@@ -0,0 +1,94 @@
+import 'package:flutter/material.dart';
+import '../../widgets/next_button.dart';
+
+class SignUpSuccessPage extends StatelessWidget {
+ const SignUpSuccessPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.075,
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Container(height: MediaQuery.of(context).size.height * 0.15),
+
+ // 성공 아이콘
+ Container(
+ width: MediaQuery.of(context).size.width * 0.25,
+ height: MediaQuery.of(context).size.width * 0.25,
+ decoration: BoxDecoration(
+ color: const Color(0xFF10B981),
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.check,
+ color: Colors.white,
+ size: 48,
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.04),
+
+ // 완료 메시지
+ const Text(
+ '회원가입이 완료되었습니다!',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 24,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF333333),
+ ),
+ textAlign: TextAlign.center,
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+
+ // 부가 설명
+ Text(
+ 'GRADI와 함께 학습 여정을 시작해보세요',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w400,
+ color: Colors.grey[600],
+ ),
+ textAlign: TextAlign.center,
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.08),
+
+ // 로그인하기 버튼
+ NextButton(
+ text: '로그인하기',
+ onPressed: () => _handleLogin(context),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.075),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _handleLogin(BuildContext context) {
+ // 로그인 페이지로 이동 (모든 이전 페이지 스택 제거)
+ Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
+ }
+}
diff --git a/frontend/lib/screens/auth/signup_terms_page.dart b/frontend/lib/screens/auth/signup_terms_page.dart
new file mode 100644
index 0000000..042aacd
--- /dev/null
+++ b/frontend/lib/screens/auth/signup_terms_page.dart
@@ -0,0 +1,426 @@
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'dart:io';
+import '../../widgets/back_button.dart' as custom;
+import '../../widgets/page_title.dart';
+import '../../widgets/next_button.dart';
+
+class SignUpTermsPage extends StatefulWidget {
+ const SignUpTermsPage({super.key});
+
+ @override
+ State createState() => _SignUpTermsPageState();
+}
+
+class _SignUpTermsPageState extends State {
+ bool _isAllTermsAgreed = false;
+ bool _isRequiredTermsAgreed = false;
+ bool _isMarketingTermsAgreed = false;
+ bool _isLoading = false;
+
+ @override
+ Widget build(BuildContext context) {
+ // 회원가입 정보 받기
+ final Map signupData =
+ ModalRoute.of(context)!.settings.arguments as Map;
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight:
+ MediaQuery.of(context).size.height -
+ MediaQuery.of(context).padding.top -
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.075,
+ ),
+ child: Column(
+ children: [
+ Container(height: MediaQuery.of(context).size.height * 0.021),
+ Row(
+ children: [
+ custom.CustomBackButton(
+ onPressed: () => Navigator.pop(context),
+ ),
+ Container(
+ width: MediaQuery.of(context).size.width * 0.05,
+ ),
+ const PageTitle(text: '권한 동의'),
+ ],
+ ),
+ Container(height: MediaQuery.of(context).size.height * 0.04),
+
+ // 전체 동의 체크박스
+ _buildAllTermsCheckbox(),
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+
+ // 구분선
+ Container(height: 1, color: const Color(0xFFE5E7EB)),
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+
+ // 필수 약관들
+ _buildRequiredTerms(),
+ Container(height: MediaQuery.of(context).size.height * 0.03),
+
+ // 선택 약관
+ _buildOptionalTerms(),
+ Container(height: MediaQuery.of(context).size.height * 0.04),
+
+ // 완료 버튼
+ NextButton(
+ text: _isLoading ? '가입 중...' : '완료',
+ onPressed: (_isRequiredTermsAgreed && !_isLoading)
+ ? () => _handleComplete(signupData)
+ : null,
+ ),
+ Container(height: MediaQuery.of(context).size.height * 0.075),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildAllTermsCheckbox() {
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ _isAllTermsAgreed = !_isAllTermsAgreed;
+ _isRequiredTermsAgreed = _isAllTermsAgreed;
+ _isMarketingTermsAgreed = _isAllTermsAgreed;
+ });
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(
+ vertical: MediaQuery.of(context).size.height * 0.015,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ children: [
+ Container(width: MediaQuery.of(context).size.width * 0.048),
+ Container(
+ width: MediaQuery.of(context).size.width * 0.06,
+ height: MediaQuery.of(context).size.width * 0.06,
+ decoration: BoxDecoration(
+ color: _isAllTermsAgreed
+ ? const Color(0xFF6366F1)
+ : Colors.white,
+ border: Border.all(
+ color: _isAllTermsAgreed
+ ? const Color(0xFF6366F1)
+ : const Color(0xFFE5E7EB),
+ width: 2,
+ ),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: _isAllTermsAgreed
+ ? const Icon(Icons.check, color: Colors.white, size: 16)
+ : null,
+ ),
+ Container(width: MediaQuery.of(context).size.width * 0.04),
+ const Expanded(
+ child: Text(
+ '전체 동의',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildRequiredTerms() {
+ return Column(
+ children: [
+ _buildTermsItem(
+ title: '서비스 이용약관',
+ isRequired: true,
+ isChecked: _isRequiredTermsAgreed,
+ onChanged: (value) {
+ setState(() {
+ _isRequiredTermsAgreed = value;
+ _updateAllTermsState();
+ });
+ },
+ ),
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+ _buildTermsItem(
+ title: '개인정보 처리방침',
+ isRequired: true,
+ isChecked: _isRequiredTermsAgreed,
+ onChanged: (value) {
+ setState(() {
+ _isRequiredTermsAgreed = value;
+ _updateAllTermsState();
+ });
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildOptionalTerms() {
+ return _buildTermsItem(
+ title: '마케팅 정보 수신 동의',
+ isRequired: false,
+ isChecked: _isMarketingTermsAgreed,
+ onChanged: (value) {
+ setState(() {
+ _isMarketingTermsAgreed = value;
+ _updateAllTermsState();
+ });
+ },
+ );
+ }
+
+ Widget _buildTermsItem({
+ required String title,
+ required bool isRequired,
+ required bool isChecked,
+ required Function(bool) onChanged,
+ }) {
+ return GestureDetector(
+ onTap: () => onChanged(!isChecked),
+ child: Row(
+ children: [
+ Container(
+ width: MediaQuery.of(context).size.width * 0.06,
+ height: MediaQuery.of(context).size.width * 0.06,
+ decoration: BoxDecoration(
+ color: isChecked ? const Color(0xFF6366F1) : Colors.white,
+ border: Border.all(
+ color: isChecked
+ ? const Color(0xFF6366F1)
+ : const Color(0xFFE5E7EB),
+ width: 2,
+ ),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: isChecked
+ ? const Icon(Icons.check, color: Colors.white, size: 16)
+ : null,
+ ),
+ Container(width: MediaQuery.of(context).size.width * 0.04),
+ Expanded(
+ child: Row(
+ children: [
+ Text(
+ title,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF333333),
+ ),
+ ),
+ if (isRequired) ...[
+ Container(width: MediaQuery.of(context).size.width * 0.02),
+ const Text(
+ '(필수)',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ],
+ const Spacer(),
+ const Icon(
+ Icons.chevron_right,
+ color: Color(0xFF9CA3AF),
+ size: 20,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _updateAllTermsState() {
+ _isAllTermsAgreed = _isRequiredTermsAgreed && _isMarketingTermsAgreed;
+ }
+
+ Future _handleComplete(Map signupData) async {
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 개발 환경에서 SSL 인증서 검증 우회 (프로덕션에서는 제거 필요)
+ HttpOverrides.global = _MyHttpOverrides();
+
+ // 서버 IP 설정
+ const String serverIp = '3.34.214.133';
+ const String url = 'https://$serverIp/sign-up';
+
+ // 이미지의 JSON 형식에 맞춰 요청 데이터 준비
+ final Map requestData = {
+ 'name': signupData['name'],
+ 'account_id': signupData['id'],
+ 'email': signupData['email'],
+ 'password': signupData['password'],
+ 'phone_number': signupData['phone'],
+ };
+
+ developer.log('POST $url');
+ developer.log('Request: $requestData');
+
+ // HTTP POST 요청
+ final response = await http.post(
+ Uri.parse(url),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode(requestData),
+ );
+
+ developer.log('Response status: ${response.statusCode}');
+ developer.log('Response body: ${response.body}');
+
+ if (!mounted) return;
+
+ setState(() {
+ _isLoading = false;
+ });
+
+ // 응답 처리
+ if (response.statusCode == 200) {
+ // 응답이 단순 문자열인지 JSON인지 확인
+ final responseBody = response.body.trim();
+
+ // 단순 문자열 응답 처리
+ if (responseBody == 'Sign-up successful') {
+ // 성공: 회원가입 완료 페이지로 이동
+ if (mounted) {
+ Navigator.pushNamed(context, '/signup-success');
+ }
+ developer.log('Sign-up successful');
+ } else if (responseBody == 'User ID already exists!') {
+ // 중복된 아이디
+ if (mounted) {
+ _showErrorDialog(message: '이미 존재하는 아이디입니다.\n다른 아이디를 사용해주세요.');
+ }
+ developer.log('Sign-up failed: User ID already exists');
+ } else {
+ // JSON 응답 시도
+ try {
+ final responseData = json.decode(responseBody);
+ if (responseData['success'] == true ||
+ responseData['sign_up'] == 'successful') {
+ // 성공: 회원가입 완료 페이지로 이동
+ if (mounted) {
+ Navigator.pushNamed(context, '/signup-success');
+ }
+ developer.log('Sign-up successful');
+ } else {
+ // 실패: 에러 팝업 표시
+ final errorMessage =
+ responseData['error'] ??
+ responseData['message'] ??
+ '회원가입에 실패했습니다.';
+ if (mounted) {
+ _showErrorDialog(message: errorMessage);
+ }
+ developer.log('Sign-up failed: $responseData');
+ }
+ } catch (e) {
+ // JSON 파싱 실패 - 서버 응답 그대로 표시
+ developer.log('Non-JSON response: $responseBody');
+ if (mounted) {
+ _showErrorDialog(
+ message: responseBody.isNotEmpty
+ ? responseBody
+ : '회원가입에 실패했습니다.',
+ );
+ }
+ }
+ }
+ } else {
+ // 서버 오류
+ if (mounted) {
+ _showErrorDialog();
+ }
+ developer.log('Server error: ${response.statusCode}');
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ _showErrorDialog();
+ }
+ developer.log('Sign-up error: $e');
+ }
+ }
+
+ void _showErrorDialog({String? message}) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text(
+ '오류',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ content: Text(
+ message ?? '문제가 발생했습니다. 잠시 후 다시 시도해주세요',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w400,
+ color: Color(0xFF666666),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text(
+ '확인',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF6366F1),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+}
+
+// 개발 환경에서 SSL 인증서 검증 우회를 위한 클래스 (프로덕션에서는 제거)
+class _MyHttpOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ }
+}
diff --git a/frontend/lib/screens/home_page.dart b/frontend/lib/screens/home_page.dart
new file mode 100644
index 0000000..97d10cf
--- /dev/null
+++ b/frontend/lib/screens/home_page.dart
@@ -0,0 +1,440 @@
+import 'package:flutter/material.dart';
+import '../widgets/continuous_learning_widget.dart';
+
+class HomePage extends StatefulWidget {
+ const HomePage({super.key});
+
+ @override
+ State createState() => _HomePageState();
+}
+
+class _HomePageState extends State {
+ // 상수 정의
+ static const double _sectionSpacing = 0.04; // 섹션 간 간격 (화면 높이의 4%)
+ static const double _smallSpacing = 0.01; // 작은 간격 (화면 높이의 1%)
+ static const double _horizontalPadding = 0.05; // 수평 패딩 (화면 너비의 5%)
+ static const double _containerPadding = 0.048; // 컨테이너 패딩 (화면 너비의 4.8%)
+ static const double _progressFactor = 0.7; // 진행률 기본값 (70%)
+ static const int _consecutiveDays = 2; // 연속 학습 일수
+
+ // TODO: DB 연동 구현 필요
+ // 1. 연속 학습 데이터 (consecutiveDays, weeklyProgress) - DB에서 사용자의 학습 기록 조회
+ // 2. 오늘의 숙제 목록 - DB에서 해당 사용자의 오늘 할당된 숙제 목록 조회
+ // 3. 오늘의 학습 현황 - DB에서 오늘 완료한 학습 과목별 진행률 조회
+ // 4. 주간 학습 현황 - DB에서 지난 7일간의 학습 통계 데이터 조회
+ // 5. 학원 정보 (헤더의 '정다훈 학원') - DB에서 사용자가 등록한 학원 정보 조회
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 헤더
+ _buildHeader(),
+
+ // 메인 콘텐츠
+ Expanded(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(
+ horizontal:
+ MediaQuery.of(context).size.width * _horizontalPadding,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ height: MediaQuery.of(context).size.height * 0.032,
+ ),
+
+ // 연속학습 섹션
+ // TODO: DB에서 사용자의 연속 학습 데이터 조회하여 동적으로 설정
+ // - consecutiveDays: DB에서 사용자의 연속 학습 일수 조회
+ // - weeklyProgress: DB에서 지난 7일간의 학습 완료 여부 조회
+ ContinuousLearningWidget(
+ consecutiveDays: _consecutiveDays, // TODO: DB 데이터로 교체
+ weeklyProgress: [
+ true, // TODO: DB에서 월요일 학습 완료 여부 조회
+ true, // TODO: DB에서 화요일 학습 완료 여부 조회
+ false, // TODO: DB에서 수요일 학습 완료 여부 조회
+ false, // TODO: DB에서 목요일 학습 완료 여부 조회
+ false, // TODO: DB에서 금요일 학습 완료 여부 조회
+ false, // TODO: DB에서 토요일 학습 완료 여부 조회
+ false, // TODO: DB에서 일요일 학습 완료 여부 조회
+ ],
+ ),
+
+ Container(
+ height:
+ MediaQuery.of(context).size.height * _sectionSpacing,
+ ),
+
+ // 오늘의 숙제 섹션
+ _buildTodayHomework(),
+
+ Container(
+ height:
+ MediaQuery.of(context).size.height * _sectionSpacing,
+ ),
+
+ // 오늘의 학습 현황 섹션
+ _buildTodayLearning(),
+
+ Container(
+ height:
+ MediaQuery.of(context).size.height * _sectionSpacing,
+ ),
+
+ // 주간 학습 현황 섹션
+ _buildWeeklyLearning(),
+
+ Container(
+ height:
+ MediaQuery.of(context).size.height * _sectionSpacing,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ MediaQuery.of(context).size.width * 0.075,
+ MediaQuery.of(context).size.height * 0.021,
+ MediaQuery.of(context).size.width * 0.075,
+ MediaQuery.of(context).size.height * 0.012,
+ ),
+ decoration: const BoxDecoration(
+ color: Color(0xFFF8F9FA),
+ boxShadow: [
+ BoxShadow(
+ color: Color(0x1A000000),
+ offset: Offset(0, 4),
+ blurRadius: 4,
+ ),
+ ],
+ ),
+ child: Column(
+ children: [
+ // 헤더 콘텐츠
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // 학원명과 드롭다운
+ // TODO: DB에서 사용자가 등록한 학원 정보 조회
+ // - 사용자가 등록한 학원명 표시
+ // - 여러 학원에 등록된 경우 드롭다운으로 선택 가능
+ Row(
+ children: [
+ const Text(
+ '정다훈 학원', // TODO: DB에서 조회한 실제 학원명으로 교체
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 20,
+ color: Color(0xFF333333),
+ ),
+ ),
+ Container(width: MediaQuery.of(context).size.width * 0.015),
+ Icon(
+ Icons.keyboard_arrow_down,
+ color: Colors.grey[600],
+ size: 24,
+ ),
+ ],
+ ),
+
+ // 메뉴 버튼
+ Icon(Icons.menu, color: Colors.grey[600], size: 24),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTodayHomework() {
+ // TODO: DB에서 오늘의 숙제 데이터 조회
+ // - 사용자가 등록한 학원의 오늘 할당된 숙제 목록 조회
+ // - 숙제 제목, 완료 여부, 마감일 등 정보 포함
+ // - 완료된 숙제는 체크박스 표시, 미완료 숙제는 빈 체크박스 표시
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSectionHeader('오늘의 숙제'),
+ Container(height: MediaQuery.of(context).size.height * _smallSpacing),
+ _buildHomeworkContainer(),
+ ],
+ );
+ }
+
+ Widget _buildHomeworkContainer() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ MediaQuery.of(context).size.width * _containerPadding,
+ MediaQuery.of(context).size.height * 0.019,
+ MediaQuery.of(context).size.width * _containerPadding,
+ MediaQuery.of(context).size.height * 0.026,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Column(children: _buildHomeworkItems()),
+ );
+ }
+
+ List _buildHomeworkItems() {
+ // TODO: DB에서 조회한 숙제 목록을 동적으로 생성
+ return [
+ _buildHomeworkItem('오늘의 숙제 리스트'),
+ _buildHomeworkItem('오늘의 숙제 리스트'),
+ _buildHomeworkItem('오늘의 숙제 리스트'),
+ _buildHomeworkItem('오늘의 숙제 리스트'),
+ _buildHomeworkItem('오늘의 숙제 리스트'),
+ ];
+ }
+
+ Widget _buildHomeworkItem(String title) {
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).size.height * 0.012,
+ ),
+ child: Row(
+ children: [
+ Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.03,
+ minWidth: 10,
+ maxHeight: MediaQuery.of(context).size.width * 0.03,
+ minHeight: 10,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFE9ECEF),
+ borderRadius: BorderRadius.circular(3),
+ ),
+ ),
+ Container(width: MediaQuery.of(context).size.width * 0.02),
+ Text(
+ title,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTodayLearning() {
+ // TODO: DB에서 오늘의 학습 현황 데이터 조회
+ // - 사용자가 오늘 학습한 과목별 진행률 조회
+ // - 각 과목의 아이콘, 진행률, 과목명 정보 포함
+ // - 학습 완료율에 따른 진행률 바 표시
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSectionHeader('오늘의 학습 현황'),
+ Container(height: MediaQuery.of(context).size.height * _smallSpacing),
+ _buildLearningContainer(),
+ ],
+ );
+ }
+
+ Widget _buildSectionHeader(String title) {
+ return Row(
+ children: [
+ Text(
+ title,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const Spacer(),
+ Icon(Icons.chevron_right, color: Colors.grey[600], size: 17),
+ ],
+ );
+ }
+
+ Widget _buildLearningContainer() {
+ final screenHeight = MediaQuery.of(context).size.height;
+ final sectionHeight = screenHeight * 0.2; // 화면 높이의 20%
+
+ return Container(
+ height: sectionHeight,
+ padding: EdgeInsets.symmetric(
+ horizontal: sectionHeight * 0.05, // 5% 수평 패딩
+ vertical: sectionHeight * 0.1, // 10% 수직 패딩
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: LayoutBuilder(
+ builder: (context, innerConstraints) {
+ final availableHeight = innerConstraints.maxHeight;
+ return _buildLearningList(availableHeight);
+ },
+ ),
+ );
+ }
+
+ Widget _buildLearningList(double availableHeight) {
+ return ListView(
+ scrollDirection: Axis.horizontal,
+ children: [
+ _buildLearningItem(
+ 'assets/images/bookcovers/workbook_2026.jpg',
+ availableHeight,
+ ),
+ SizedBox(width: availableHeight * 0.15),
+ _buildLearningItem(
+ 'assets/images/bookcovers/workbook_2025.jpg',
+ availableHeight,
+ ),
+ SizedBox(width: availableHeight * 0.15),
+ _buildLearningItem(
+ 'assets/images/bookcovers/workbook_2024.jpg',
+ availableHeight,
+ ),
+ SizedBox(width: availableHeight * 0.15),
+ ],
+ );
+ }
+
+ Widget _buildLearningItem(String imagePath, double containerHeight) {
+ // 표지와 상태바가 컨테이너 높이의 90%를 차지하도록 설정 (더 크게!)
+ final totalItemHeight = containerHeight * 0.9;
+
+ // 표지 높이: 전체 아이템 높이의 80%
+ final bookHeight = totalItemHeight * 0.8;
+ // 3:4 비율에 맞춰 너비 계산
+ final bookWidth = bookHeight * 0.75; // 3/4 = 0.75
+
+ return SizedBox(
+ height: containerHeight, // 전체 컨테이너 높이 사용
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center, // 세로축 중앙 정렬
+ children: [
+ // 학습 아이콘 (3:4 비율 사각형) - 교재 표지 이미지
+ Container(
+ width: bookWidth, // 상대 크기 (3:4 비율)
+ height: bookHeight, // 상대 크기 (컨테이너 높이의 80%)
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
+ borderRadius: BorderRadius.circular(
+ bookHeight * 0.1,
+ ), // 상대적 둥근 모서리
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(bookHeight * 0.1),
+ child: Image.asset(
+ imagePath, // 교재 표지 이미지 경로
+ width: bookWidth,
+ height: bookHeight,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ // 이미지 로드 실패 시 기본 아이콘 표시
+ return Icon(
+ Icons.book,
+ color: Colors.grey[400],
+ size: bookHeight * 0.4,
+ );
+ },
+ ),
+ ),
+ ),
+
+ SizedBox(height: totalItemHeight * 0.1), // 표지와 진행률 바 사이 간격
+ // 진행률 바
+ Container(
+ width: bookWidth,
+ height: totalItemHeight * 0.1, // 전체 아이템 높이의 10%
+ decoration: BoxDecoration(
+ color: Colors.grey[200],
+ borderRadius: BorderRadius.circular(totalItemHeight * 0.05),
+ ),
+ child: FractionallySizedBox(
+ alignment: Alignment.centerLeft,
+ widthFactor: _progressFactor, // 70% 진행률
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF636ACF)],
+ ),
+ borderRadius: BorderRadius.circular(totalItemHeight * 0.05),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWeeklyLearning() {
+ // TODO: DB에서 주간 학습 현황 데이터 조회
+ // - 지난 7일간의 학습 통계 데이터 조회
+ // - 일별 학습 시간, 완료한 과목 수, 성취도 등 정보 포함
+ // - 차트 라이브러리(fl_chart 등)를 사용하여 시각화 구현
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildSectionHeader('주간 학습 현황'),
+ Container(height: MediaQuery.of(context).size.height * _smallSpacing),
+ _buildWeeklyChartContainer(),
+ ],
+ );
+ }
+
+ Widget _buildWeeklyChartContainer() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ MediaQuery.of(context).size.width * _containerPadding,
+ MediaQuery.of(context).size.height * 0.019,
+ MediaQuery.of(context).size.width * _containerPadding,
+ MediaQuery.of(context).size.height * 0.026,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Container(
+ constraints: BoxConstraints(
+ minHeight: MediaQuery.of(context).size.height * 0.1,
+ maxHeight: MediaQuery.of(context).size.height * 0.15,
+ ),
+ child: const Center(
+ child: Text(
+ '주간 학습 현황 차트', // TODO: 실제 차트로 교체 (fl_chart 등 사용)
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: Color(0xFF666666),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/main_navigation_page.dart b/frontend/lib/screens/main_navigation_page.dart
new file mode 100644
index 0000000..40cdd24
--- /dev/null
+++ b/frontend/lib/screens/main_navigation_page.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import '../widgets/bottom_navigation_widget.dart';
+import 'home_page.dart';
+import 'workbook/workbook_page.dart';
+import 'academy/academy_page.dart';
+
+class MainNavigationPage extends StatefulWidget {
+ const MainNavigationPage({super.key});
+
+ @override
+ State createState() => _MainNavigationPageState();
+}
+
+class _MainNavigationPageState extends State {
+ int _currentIndex = 0;
+
+ // 모든 탭 페이지들
+ final List _pages = [
+ const HomePage(),
+ const WorkbookPage(),
+ const PlaceholderPage(title: '이미지업로드'),
+ const AcademyPage(),
+ const PlaceholderPage(title: '마이페이지'),
+ const PlaceholderPage(title: '알람'),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: IndexedStack(index: _currentIndex, children: _pages),
+ bottomNavigationBar: BottomNavigationWidget(
+ currentIndex: _currentIndex,
+ onTabChanged: (index) {
+ setState(() {
+ _currentIndex = index;
+ });
+ },
+ ),
+ );
+ }
+}
+
+// 미구현 페이지들을 위한 플레이스홀더
+class PlaceholderPage extends StatelessWidget {
+ final String title;
+
+ const PlaceholderPage({super.key, required this.title});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.construction, size: 64, color: Colors.grey[400]),
+ const SizedBox(height: 16),
+ Text(
+ '$title 페이지',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 18,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '구현 예정입니다',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 14,
+ color: Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/screens/workbook/workbook_page.dart b/frontend/lib/screens/workbook/workbook_page.dart
new file mode 100644
index 0000000..1353f68
--- /dev/null
+++ b/frontend/lib/screens/workbook/workbook_page.dart
@@ -0,0 +1,600 @@
+import 'package:flutter/material.dart';
+import '../../widgets/continuous_learning_widget.dart';
+
+enum WorkbookViewType {
+ byClass, // 클래스 순
+ byWorkbook, // 문제집 순
+}
+
+class WorkbookPage extends StatefulWidget {
+ const WorkbookPage({super.key});
+
+ @override
+ State createState() => _WorkbookPageState();
+}
+
+class _WorkbookPageState extends State {
+ WorkbookViewType _currentView = WorkbookViewType.byClass;
+
+ // TODO: Fetch data from server
+ // 클래스 순 데이터
+ final List _classData = [
+ ClassData(
+ className: '오세종 선생님 3반',
+ lastStudyDate: '2025.10.09',
+ workbooks: [
+ WorkbookInfo(
+ name: '블랙라벨 중등수학 1-1',
+ lastStudyDate: '2025.10.09',
+ progress: 65,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_Blacklabel.png',
+ ),
+ WorkbookInfo(
+ name: '라이트쎈 중등수학 1-1',
+ lastStudyDate: '2025.10.06',
+ progress: 40,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_LightSsen.png',
+ ),
+ ],
+ ),
+ ClassData(
+ className: '조성재 선생님 1반',
+ lastStudyDate: '2025.10.07',
+ workbooks: [
+ WorkbookInfo(
+ name: '100발 100중 중등수학 2-2',
+ lastStudyDate: '2025.10.07',
+ progress: 55,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_100to100.png',
+ ),
+ ],
+ ),
+ ClassData(
+ className: '최상일 선생님 2반',
+ lastStudyDate: '2025.10.05',
+ workbooks: [
+ WorkbookInfo(
+ name: '수능완성 영어 2026',
+ lastStudyDate: '2025.10.05',
+ progress: 75,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2026.jpg',
+ ),
+ WorkbookInfo(
+ name: '수능완성 영어 2025',
+ lastStudyDate: '2025.10.03',
+ progress: 90,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2025.jpg',
+ ),
+ WorkbookInfo(
+ name: '수능완성 영어 2024',
+ lastStudyDate: '2025.10.01',
+ progress: 100,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2024.jpg',
+ ),
+ ],
+ ),
+ ];
+
+ // 문제집 순 데이터 (모든 문제집을 마지막 학습일 순으로 정렬)
+ final List _workbookData = [
+ WorkbookData(
+ workbookName: '블랙라벨 중등수학 1-1',
+ lastStudyDate: '2025.10.09',
+ progress: 65,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_Blacklabel.png',
+ className: '오세종 선생님 3반',
+ ),
+ WorkbookData(
+ workbookName: '100발 100중 중등수학 2-2',
+ lastStudyDate: '2025.10.07',
+ progress: 55,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_100to100.png',
+ className: '조성재 선생님 1반',
+ ),
+ WorkbookData(
+ workbookName: '라이트쎈 중등수학 1-1',
+ lastStudyDate: '2025.10.06',
+ progress: 40,
+ thumbnailPath: 'assets/images/bookcovers/BookCover_LightSsen.png',
+ className: '오세종 선생님 3반',
+ ),
+ WorkbookData(
+ workbookName: '수능완성 영어 2026',
+ lastStudyDate: '2025.10.05',
+ progress: 75,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2026.jpg',
+ className: '최상일 선생님 2반',
+ ),
+ WorkbookData(
+ workbookName: '수능완성 영어 2025',
+ lastStudyDate: '2025.10.03',
+ progress: 90,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2025.jpg',
+ className: '최상일 선생님 2반',
+ ),
+ WorkbookData(
+ workbookName: '수능완성 영어 2024',
+ lastStudyDate: '2025.10.01',
+ progress: 100,
+ thumbnailPath: 'assets/images/bookcovers/workbook_2024.jpg',
+ className: '최상일 선생님 2반',
+ ),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Column(
+ children: [
+ // 헤더
+ _buildHeader(),
+
+ // 메인 콘텐츠 (스크롤 가능)
+ Expanded(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.05,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ height: MediaQuery.of(context).size.height * 0.032,
+ ),
+
+ // 주간 캘린더
+ _buildWeeklyCalendar(),
+
+ Container(
+ height: MediaQuery.of(context).size.height * 0.01,
+ ),
+
+ // 토글 버튼
+ _buildToggle(),
+
+ Container(
+ height: MediaQuery.of(context).size.height * 0.01,
+ ),
+
+ // 메인 콘텐츠
+ _currentView == WorkbookViewType.byClass
+ ? _buildClassView()
+ : _buildWorkbookView(),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ MediaQuery.of(context).size.width * 0.05,
+ MediaQuery.of(context).size.height * 0.021,
+ MediaQuery.of(context).size.width * 0.05,
+ MediaQuery.of(context).size.height * 0.012,
+ ),
+ decoration: const BoxDecoration(color: Colors.white),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ '문제집',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 20,
+ color: Color(0xFF333333),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.menu, color: Color(0xFF333333)),
+ onPressed: () {
+ // TODO: 메뉴 기능 구현
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('메뉴 기능 구현 예정')));
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWeeklyCalendar() {
+ return ContinuousLearningWidget(
+ consecutiveDays: 2,
+ weeklyProgress: const [true, true, false, false, false, false, false],
+ );
+ }
+
+ Widget _buildToggle() {
+ return Row(
+ children: [
+ // 토글 스위치
+ GestureDetector(
+ onTap: () {
+ setState(() {
+ _currentView = _currentView == WorkbookViewType.byClass
+ ? WorkbookViewType.byWorkbook
+ : WorkbookViewType.byClass;
+ });
+ },
+ child: Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.12,
+ minWidth: 40,
+ maxHeight: 20,
+ minHeight: 16,
+ ),
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF7C3AED)],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: AnimatedAlign(
+ duration: const Duration(milliseconds: 200),
+ alignment: _currentView == WorkbookViewType.byClass
+ ? Alignment.centerLeft
+ : Alignment.centerRight,
+ child: Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.05,
+ minWidth: 7,
+ maxHeight: MediaQuery.of(context).size.width * 0.05,
+ minHeight: 7,
+ ),
+ margin: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.005,
+ ),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+ ),
+ ),
+ Container(width: MediaQuery.of(context).size.width * 0.03),
+ // 토글 라벨
+ Text(
+ _currentView == WorkbookViewType.byClass ? '최근 클래스 순' : '최근 문제집 순',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildClassView() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _classData.length,
+ separatorBuilder: (context, index) =>
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+ itemBuilder: (context, index) {
+ return _buildClassCard(_classData[index]);
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildClassCard(ClassData classData) {
+ return Container(
+ padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withAlpha(13),
+ offset: const Offset(0, 2),
+ blurRadius: 8,
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 클래스명
+ Text(
+ classData.className,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 16,
+ color: Color(0xFF333333),
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+
+ // 문제집 썸네일과 진행률 리스트
+ Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: List.generate(
+ classData.workbooks.length,
+ (index) => Padding(
+ padding: EdgeInsets.only(
+ right: index < classData.workbooks.length - 1
+ ? MediaQuery.of(context).size.width * 0.04
+ : 0,
+ ),
+ child: _buildWorkbookProgress(classData.workbooks[index]),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildWorkbookProgress(WorkbookInfo workbook) {
+ final screenWidth = MediaQuery.of(context).size.width;
+ final thumbnailWidth = screenWidth * 0.15; // 화면 너비의 15%
+ final thumbnailHeight = thumbnailWidth * 1.33; // 3:4 비율 유지
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 문제집 썸네일
+ ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: Container(
+ width: thumbnailWidth,
+ height: thumbnailHeight,
+ color: const Color(0xFFE9ECEF),
+ child: Image.asset(
+ workbook.thumbnailPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return const Center(
+ child: Icon(Icons.book, color: Color(0xFF999999)),
+ );
+ },
+ ),
+ ),
+ ),
+ Container(height: MediaQuery.of(context).size.height * 0.01),
+ // 진행률 바
+ SizedBox(
+ width: thumbnailWidth,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: Container(
+ height: 6,
+ decoration: const BoxDecoration(color: Color(0xFFE9ECEF)),
+ child: FractionallySizedBox(
+ alignment: Alignment.centerLeft,
+ widthFactor: workbook.progress / 100,
+ child: Container(
+ decoration: const BoxDecoration(
+ gradient: LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF7C3AED)],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildWorkbookView() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _workbookData.length,
+ separatorBuilder: (context, index) =>
+ Container(height: MediaQuery.of(context).size.height * 0.02),
+ itemBuilder: (context, index) {
+ return _buildWorkbookCard(_workbookData[index]);
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildWorkbookCard(WorkbookData workbookData) {
+ return Container(
+ padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withAlpha(13),
+ offset: const Offset(0, 2),
+ blurRadius: 8,
+ ),
+ ],
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 문제집 썸네일
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.18,
+ minWidth: 60,
+ maxHeight: MediaQuery.of(context).size.width * 0.23,
+ minHeight: 80,
+ ),
+ color: const Color(0xFFE74C3C),
+ child: Image.asset(
+ workbookData.thumbnailPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return const Center(
+ child: Text(
+ 'blacklabel',
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 10,
+ color: Colors.white,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+
+ Container(width: MediaQuery.of(context).size.width * 0.04),
+
+ // 문제집 정보
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 문제집명
+ Text(
+ workbookData.workbookName,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w700,
+ fontSize: 16,
+ color: Color(0xFF333333),
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.01),
+
+ // 학습 정보
+ Text(
+ '${workbookData.className}에서 진행 중',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFF666666),
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.005),
+
+ // 마지막 학습일
+ Text(
+ '마지막 학습 일 ${workbookData.lastStudyDate}',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w400,
+ fontSize: 12,
+ color: Color(0xFF999999),
+ ),
+ ),
+
+ Container(height: MediaQuery.of(context).size.height * 0.015),
+
+ // 진행률
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: Container(
+ height: 8,
+ decoration: const BoxDecoration(
+ color: Color(0xFFE9ECEF),
+ ),
+ child: FractionallySizedBox(
+ alignment: Alignment.centerLeft,
+ widthFactor: workbookData.progress / 100,
+ child: Container(
+ decoration: const BoxDecoration(
+ gradient: LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF7C3AED)],
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// 클래스 데이터 모델
+class ClassData {
+ final String className;
+ final String lastStudyDate;
+ final List workbooks;
+
+ ClassData({
+ required this.className,
+ required this.lastStudyDate,
+ required this.workbooks,
+ });
+}
+
+// 문제집 정보 모델 (클래스 내부용)
+class WorkbookInfo {
+ final String name;
+ final String lastStudyDate;
+ final int progress;
+ final String thumbnailPath;
+
+ WorkbookInfo({
+ required this.name,
+ required this.lastStudyDate,
+ required this.progress,
+ required this.thumbnailPath,
+ });
+}
+
+// 문제집 데이터 모델 (문제집 순 페이지용)
+class WorkbookData {
+ final String workbookName;
+ final String lastStudyDate;
+ final int progress;
+ final String thumbnailPath;
+ final String className;
+
+ WorkbookData({
+ required this.workbookName,
+ required this.lastStudyDate,
+ required this.progress,
+ required this.thumbnailPath,
+ required this.className,
+ });
+}
diff --git a/frontend/lib/theme/app_theme.dart b/frontend/lib/theme/app_theme.dart
new file mode 100644
index 0000000..4416c86
--- /dev/null
+++ b/frontend/lib/theme/app_theme.dart
@@ -0,0 +1,180 @@
+import 'package:flutter/material.dart';
+
+class AppTheme {
+ // Colors from Figma design system
+ static const Color primaryColor = Color(0xFFAC5BF8);
+ static const Color secondaryColor = Color(0xFF636ACF);
+ static const Color backgroundColor = Color(0xFFF3F3F3);
+ static const Color textPrimary = Color(0xFF5C5C5C);
+ static const Color textSecondary = Color(0xFFA0A0A0);
+ static const Color accentColor = Color(0xFF666EDE);
+ static const Color errorColor = Color(0xFFFF4258);
+ static const Color whiteColor = Color(0xFFFFFFFF);
+
+ // Gradient colors
+ static const LinearGradient primaryGradient = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [primaryColor, secondaryColor],
+ stops: [0.0, 1.0],
+ transform: GradientRotation(144 * 3.14159 / 180), // 144 degrees
+ );
+
+ static ThemeData get lightTheme {
+ return ThemeData(
+ useMaterial3: true,
+ fontFamily: 'Pretendard',
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: primaryColor,
+ brightness: Brightness.light,
+ primary: primaryColor,
+ secondary: secondaryColor,
+ surface: whiteColor,
+ error: errorColor,
+ onPrimary: whiteColor,
+ onSecondary: whiteColor,
+ onSurface: textPrimary,
+ onError: whiteColor,
+ ),
+
+ // Text themes
+ textTheme: const TextTheme(
+ displayLarge: TextStyle(
+ fontFamily: 'AppleSDGothicNeoH00',
+ fontWeight: FontWeight.w400,
+ fontSize: 88.98,
+ letterSpacing: -0.06,
+ color: textPrimary,
+ ),
+ headlineLarge: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 24,
+ color: textPrimary,
+ ),
+ headlineMedium: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 20,
+ color: textPrimary,
+ ),
+ titleLarge: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 18,
+ color: textPrimary,
+ ),
+ titleMedium: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 16,
+ color: textPrimary,
+ ),
+ bodyLarge: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ color: textPrimary,
+ ),
+ bodyMedium: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 15,
+ color: textPrimary,
+ ),
+ bodySmall: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 14,
+ color: textSecondary,
+ ),
+ labelLarge: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 15,
+ color: textPrimary,
+ ),
+ ),
+
+ // Input decoration theme
+ inputDecorationTheme: InputDecorationTheme(
+ filled: true,
+ fillColor: backgroundColor,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(5),
+ borderSide: BorderSide.none,
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(5),
+ borderSide: BorderSide.none,
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(5),
+ borderSide: const BorderSide(color: accentColor, width: 2),
+ ),
+ errorBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(5),
+ borderSide: const BorderSide(color: errorColor, width: 2),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(5),
+ borderSide: const BorderSide(color: errorColor, width: 2),
+ ),
+ hintStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 15,
+ color: textSecondary,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+
+ // Elevated button theme
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: primaryColor,
+ foregroundColor: whiteColor,
+ minimumSize: const Size(342, 50),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
+ textStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 16,
+ ),
+ ),
+ ),
+
+ // Text button theme
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ foregroundColor: accentColor,
+ textStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ ),
+ ),
+ ),
+
+ // App bar theme
+ appBarTheme: const AppBarTheme(
+ backgroundColor: whiteColor,
+ foregroundColor: textPrimary,
+ elevation: 0,
+ centerTitle: true,
+ titleTextStyle: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 20,
+ color: textPrimary,
+ ),
+ ),
+
+ // Scaffold theme
+ scaffoldBackgroundColor: whiteColor,
+ );
+ }
+}
diff --git a/frontend/lib/widgets/app_logo.dart b/frontend/lib/widgets/app_logo.dart
new file mode 100644
index 0000000..e002790
--- /dev/null
+++ b/frontend/lib/widgets/app_logo.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+class AppLogo extends StatelessWidget {
+ final double? width;
+ final double? height;
+ final double widthFactor; // 화면 너비 대비 로고 너비 비율
+
+ const AppLogo({super.key, this.width, this.height, this.widthFactor = 0.7});
+
+ @override
+ Widget build(BuildContext context) {
+ final double screenWidth = MediaQuery.of(context).size.width;
+ final double targetWidth = width ?? (screenWidth * widthFactor);
+
+ return Container(
+ width: targetWidth,
+ height: height,
+ constraints: BoxConstraints(
+ maxWidth: screenWidth * 0.9, // 화면 너비의 90%를 넘지 않음
+ minWidth: 200, // 최소 너비 보장
+ maxHeight: height ?? 200, // 최대 높이 제한
+ ),
+ child: FittedBox(
+ fit: BoxFit.contain, // 주어진 폭 안에서 자동 스케일
+ child: ShaderMask(
+ shaderCallback: (bounds) => const LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Color(0xFFAC5BF8), // rgba(172, 91, 248, 1)
+ Color(0xFF636ACF), // rgba(99, 106, 207, 1)
+ ],
+ stops: [0.09, 0.92],
+ transform: GradientRotation(2.67), // 153 degrees in radians
+ ).createShader(bounds),
+ child: const Text(
+ 'GRADI',
+ style: TextStyle(
+ fontFamily: 'AppleSDGothicNeoH00',
+ fontSize: 96, // 기준 폰트 크기 (FittedBox가 스케일 조정)
+ fontWeight: FontWeight.w400,
+ height: 1.491,
+ letterSpacing: -0.06,
+ color: Colors.white, // This will be masked by the gradient
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/back_button.dart b/frontend/lib/widgets/back_button.dart
new file mode 100644
index 0000000..b452af5
--- /dev/null
+++ b/frontend/lib/widgets/back_button.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+
+class CustomBackButton extends StatelessWidget {
+ final VoidCallback? onPressed;
+ final double? width;
+ final double? height;
+
+ const CustomBackButton({super.key, this.onPressed, this.width, this.height});
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: width ?? 38,
+ height: height ?? 38,
+ child: IconButton(
+ onPressed: onPressed ?? () => Navigator.of(context).pop(),
+ icon: CustomPaint(
+ size: const Size(9.5, 19),
+ painter: BackArrowPainter(),
+ ),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ );
+ }
+}
+
+class BackArrowPainter extends CustomPainter {
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..color = const Color(0xFF5C5C5C)
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 3.17;
+
+ final path = Path();
+ // Draw left arrow
+ path.moveTo(size.width * 0.8, 0);
+ path.lineTo(0, size.height * 0.5);
+ path.lineTo(size.width * 0.8, size.height);
+
+ canvas.drawPath(path, paint);
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
diff --git a/frontend/lib/widgets/bottom_navigation_widget.dart b/frontend/lib/widgets/bottom_navigation_widget.dart
new file mode 100644
index 0000000..7f35359
--- /dev/null
+++ b/frontend/lib/widgets/bottom_navigation_widget.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+class BottomNavigationWidget extends StatelessWidget {
+ final int currentIndex;
+ final Function(int) onTabChanged;
+
+ const BottomNavigationWidget({
+ super.key,
+ required this.currentIndex,
+ required this.onTabChanged,
+ });
+
+ void _handleTabChange(int index) {
+ if (index == currentIndex) return; // 현재 탭이면 아무것도 하지 않음
+ onTabChanged(index);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 70,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ border: Border(top: BorderSide(color: Color(0xFFE9ECEF))),
+ ),
+ child: Row(
+ children: [
+ _buildNavItem('Home', '홈', 0),
+ _buildNavItem('Textbook', '문제집', 1),
+ _buildNavItem('Upload_image', '이미지업로드', 2),
+ _buildNavItem('Academy', '학원', 3),
+ _buildNavItem('Mypage', '마이페이지', 4),
+ _buildNavItem('Alarm', '알람', 5),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildNavItem(String iconName, String label, int index) {
+ final isActive = currentIndex == index;
+ final iconPath = isActive
+ ? 'assets/images/icons/${iconName}_selected.svg'
+ : 'assets/images/icons/${iconName}_unselected.svg';
+
+ return Expanded(
+ child: GestureDetector(
+ onTap: () => _handleTabChange(index),
+ child: Container(
+ color: Colors.transparent,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // SVG 아이콘
+ SvgPicture.asset(iconPath, width: 24, height: 24),
+ const SizedBox(height: 4),
+ // 텍스트
+ Text(
+ label,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
+ fontSize: 10,
+ color: isActive
+ ? const Color(0xFFAC5BF8)
+ : const Color(0xFFADADAD),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/continuous_learning_widget.dart b/frontend/lib/widgets/continuous_learning_widget.dart
new file mode 100644
index 0000000..9df3ca4
--- /dev/null
+++ b/frontend/lib/widgets/continuous_learning_widget.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+
+class ContinuousLearningWidget extends StatelessWidget {
+ final int consecutiveDays;
+ final List weeklyProgress;
+
+ const ContinuousLearningWidget({
+ super.key,
+ required this.consecutiveDays,
+ required this.weeklyProgress,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '$consecutiveDays일 연속으로 학습하고 있어요',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
+
+ const SizedBox(height: 8),
+
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 19, vertical: 15),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF8F9FA),
+ border: Border.all(color: const Color(0xFFE9ECEF)),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ _buildDayCard('월', weeklyProgress[0]),
+ _buildDayCard('화', weeklyProgress[1]),
+ _buildDayCard('수', weeklyProgress[2]),
+ _buildDayCard('목', weeklyProgress[3]),
+ _buildDayCard('금', weeklyProgress[4]),
+ _buildDayCard('토', weeklyProgress[5]),
+ _buildDayCard('일', weeklyProgress[6]),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildDayCard(String day, bool isActive) {
+ return Column(
+ children: [
+ Container(
+ width: 36,
+ height: 34,
+ decoration: BoxDecoration(
+ gradient: isActive
+ ? const LinearGradient(
+ colors: [Color(0xFFAC5BF8), Color(0xFF636ACF)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ )
+ : null,
+ color: isActive ? null : Colors.grey[200],
+ borderRadius: BorderRadius.circular(8),
+ boxShadow: isActive
+ ? [
+ BoxShadow(
+ color: const Color(0xFFAC5BF8).withValues(alpha: 0.3),
+ blurRadius: 4,
+ offset: const Offset(0, 0),
+ ),
+ ]
+ : null,
+ ),
+ child: Center(
+ child: Text(
+ day,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
+ color: isActive ? Colors.white : const Color(0xFF666666),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/lib/widgets/error_input_field.dart b/frontend/lib/widgets/error_input_field.dart
new file mode 100644
index 0000000..bad69cf
--- /dev/null
+++ b/frontend/lib/widgets/error_input_field.dart
@@ -0,0 +1,119 @@
+import 'package:flutter/material.dart';
+
+class ErrorInputField extends StatelessWidget {
+ final String label;
+ final String placeholder;
+ final TextEditingController? controller;
+ final bool obscureText;
+ final TextInputType? keyboardType;
+ final ValueChanged? onChanged;
+ final String? errorText;
+ final bool hasError;
+ final double? width;
+ final double? height;
+
+ const ErrorInputField({
+ super.key,
+ required this.label,
+ required this.placeholder,
+ this.controller,
+ this.obscureText = false,
+ this.keyboardType,
+ this.onChanged,
+ this.errorText,
+ this.hasError = false,
+ this.width,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: width ?? 342,
+ height: height ?? (hasError ? 102 : 76),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Label
+ Text(
+ label,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+
+ const SizedBox(height: 8),
+
+ // Input Field
+ Container(
+ width: 342,
+ height: 50,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: hasError
+ ? Border.all(
+ color: const Color(
+ 0xFFAC5BF8,
+ ), // Gradient color for error border
+ width: 2,
+ )
+ : null,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: TextField(
+ controller: controller,
+ obscureText: obscureText,
+ keyboardType: keyboardType,
+ onChanged: onChanged,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: hasError
+ ? const Color(0xFFFF4258)
+ : const Color(0xFFA0A0A0),
+ ),
+ decoration: InputDecoration(
+ hintText: placeholder,
+ hintStyle: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: hasError
+ ? const Color(0xFFFF4258)
+ : const Color(0xFFA0A0A0),
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+ ),
+ ),
+
+ // Error Text
+ if (hasError && errorText != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ errorText!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFAC5BF8), // Gradient color for error text
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/input_field.dart b/frontend/lib/widgets/input_field.dart
new file mode 100644
index 0000000..c874908
--- /dev/null
+++ b/frontend/lib/widgets/input_field.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+
+class InputField extends StatelessWidget {
+ final String placeholder;
+ final TextEditingController? controller;
+ final bool obscureText;
+ final TextInputType? keyboardType;
+ final ValueChanged? onChanged;
+ final double? width;
+ final double? height;
+ final bool isError;
+
+ const InputField({
+ super.key,
+ required this.placeholder,
+ this.controller,
+ this.obscureText = false,
+ this.keyboardType,
+ this.onChanged,
+ this.width,
+ this.height,
+ this.isError = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: width ?? 342,
+ height: height ?? 50,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF3F3F3),
+ borderRadius: BorderRadius.circular(5),
+ border: isError
+ ? Border.all(color: const Color(0xFFFF4258), width: 1)
+ : null,
+ ),
+ child: TextField(
+ controller: controller,
+ obscureText: obscureText,
+ keyboardType: keyboardType,
+ onChanged: onChanged,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: isError ? const Color(0xFFFF4258) : const Color(0xFFA0A0A0),
+ ),
+ decoration: InputDecoration(
+ hintText: placeholder,
+ hintStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/labeled_input_field.dart b/frontend/lib/widgets/labeled_input_field.dart
new file mode 100644
index 0000000..33754c0
--- /dev/null
+++ b/frontend/lib/widgets/labeled_input_field.dart
@@ -0,0 +1,112 @@
+import 'package:flutter/material.dart';
+
+class LabeledInputField extends StatelessWidget {
+ final String label;
+ final String placeholder;
+ final TextEditingController? controller;
+ final bool obscureText;
+ final TextInputType? keyboardType;
+ final ValueChanged? onChanged;
+ final double? width;
+ final double? height;
+ final bool isError;
+ final String? errorMessage;
+
+ const LabeledInputField({
+ super.key,
+ required this.label,
+ required this.placeholder,
+ this.controller,
+ this.obscureText = false,
+ this.keyboardType,
+ this.onChanged,
+ this.width,
+ this.height,
+ this.isError = false,
+ this.errorMessage,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: width ?? 342,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Label
+ Text(
+ label,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF5C5C5C),
+ ),
+ ),
+
+ const SizedBox(height: 8),
+
+ // Input Field
+ Container(
+ width: 342,
+ height: 50,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF3F3F3),
+ borderRadius: BorderRadius.circular(5),
+ border: isError
+ ? Border.all(color: const Color(0xFFFF4258), width: 1)
+ : null,
+ ),
+ child: TextField(
+ controller: controller,
+ obscureText: obscureText,
+ keyboardType: keyboardType,
+ onChanged: onChanged,
+ style: TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: isError
+ ? const Color(0xFFFF4258)
+ : const Color(0xFFA0A0A0),
+ ),
+ decoration: InputDecoration(
+ hintText: placeholder,
+ hintStyle: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 16,
+ ),
+ ),
+ ),
+ ),
+
+ // Error Message
+ if (isError && errorMessage != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ errorMessage!,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ height: 1.33,
+ color: Color(0xFFFF4258),
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/links_section.dart b/frontend/lib/widgets/links_section.dart
new file mode 100644
index 0000000..c4276d5
--- /dev/null
+++ b/frontend/lib/widgets/links_section.dart
@@ -0,0 +1,80 @@
+import 'package:flutter/material.dart';
+
+class LinksSection extends StatelessWidget {
+ final VoidCallback? onSignUp;
+ final VoidCallback? onFindID;
+ final VoidCallback? onFindPW;
+
+ const LinksSection({super.key, this.onSignUp, this.onFindID, this.onFindPW});
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: 244,
+ height: 17,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ // 회원가입
+ GestureDetector(
+ onTap: onSignUp,
+ child: Text(
+ '회원가입',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF666EDE),
+ ),
+ ),
+ ),
+
+ // 구분선 1
+ SizedBox(
+ width: 1,
+ height: 14,
+ child: Container(color: const Color(0xFFCBCBCB)),
+ ),
+
+ // 아이디 찾기
+ GestureDetector(
+ onTap: onFindID,
+ child: Text(
+ '아이디 찾기',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ ),
+
+ // 구분선 2
+ SizedBox(
+ width: 1,
+ height: 14,
+ child: Container(color: const Color(0xFFCBCBCB)),
+ ),
+
+ // 비밀번호 찾기
+ GestureDetector(
+ onTap: onFindPW,
+ child: Text(
+ '비밀번호 찾기',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/login_button.dart b/frontend/lib/widgets/login_button.dart
new file mode 100644
index 0000000..b6642c4
--- /dev/null
+++ b/frontend/lib/widgets/login_button.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+
+class LoginButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+ final double? width;
+ final double? height;
+
+ const LoginButton({
+ super.key,
+ required this.text,
+ this.onPressed,
+ this.width,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: width ?? 342,
+ height: height ?? 50,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Color(0xFFAC5BF8), // rgba(172, 91, 248, 1)
+ Color(0xFF636ACF), // rgba(99, 106, 207, 1)
+ ],
+ stops: [0.0, 1.0],
+ transform: GradientRotation(2.51), // 144 degrees in radians
+ ),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: ElevatedButton(
+ onPressed: onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
+ ),
+ child: Text(
+ text,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/next_button.dart b/frontend/lib/widgets/next_button.dart
new file mode 100644
index 0000000..aae26e3
--- /dev/null
+++ b/frontend/lib/widgets/next_button.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+
+class NextButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+ final double? width;
+ final double? height;
+
+ const NextButton({
+ super.key,
+ this.text = '다음',
+ this.onPressed,
+ this.width,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: width ?? 342,
+ height: height ?? 50,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Color(0xFFAC5BF8), // rgba(172, 91, 248, 1)
+ Color(0xFF636ACF), // rgba(99, 106, 207, 1)
+ ],
+ stops: [0.0, 1.0],
+ transform: GradientRotation(2.51), // 144 degrees in radians
+ ),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: ElevatedButton(
+ onPressed: onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
+ ),
+ child: Text(
+ text,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/page_title.dart b/frontend/lib/widgets/page_title.dart
new file mode 100644
index 0000000..afee855
--- /dev/null
+++ b/frontend/lib/widgets/page_title.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+class PageTitle extends StatelessWidget {
+ final String text;
+ final double? width;
+ final double? height;
+ final TextAlign? textAlign;
+
+ const PageTitle({
+ super.key,
+ required this.text,
+ this.width,
+ this.height,
+ this.textAlign,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: width,
+ child: Text(
+ text,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ height: 1.193,
+ color: Color(0xFF5C5C5C),
+ ),
+ textAlign: textAlign ?? TextAlign.center,
+ overflow: TextOverflow.visible,
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/sns_button.dart b/frontend/lib/widgets/sns_button.dart
new file mode 100644
index 0000000..e8821f1
--- /dev/null
+++ b/frontend/lib/widgets/sns_button.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+
+enum SNSProvider { kakao, google }
+
+class SNSButton extends StatelessWidget {
+ final SNSProvider provider;
+ final VoidCallback? onPressed;
+ final double? width;
+ final double? height;
+
+ const SNSButton({
+ super.key,
+ required this.provider,
+ this.onPressed,
+ this.width,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onPressed,
+ child: SizedBox(
+ width: width ?? 50,
+ height: height ?? 50,
+ child: _buildIcon(),
+ ),
+ );
+ }
+
+ Widget _buildIcon() {
+ switch (provider) {
+ case SNSProvider.kakao:
+ return Image.asset(
+ 'assets/images/social_login/Button_Kakao.png',
+ width: width ?? 50,
+ height: height ?? 50,
+ fit: BoxFit.contain,
+ );
+ case SNSProvider.google:
+ return Image.asset(
+ 'assets/images/social_login/Button_Google.png',
+ width: width ?? 50,
+ height: height ?? 50,
+ fit: BoxFit.contain,
+ );
+ }
+ }
+}
diff --git a/frontend/lib/widgets/sns_divider.dart b/frontend/lib/widgets/sns_divider.dart
new file mode 100644
index 0000000..5a14f39
--- /dev/null
+++ b/frontend/lib/widgets/sns_divider.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+
+class SNSDivider extends StatelessWidget {
+ final String text;
+ final double? width;
+ final double? height;
+
+ const SNSDivider({
+ super.key,
+ this.text = 'SNS 간편 로그인',
+ this.width,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: width ?? 339,
+ height: height ?? 17,
+ child: Row(
+ children: [
+ // Left line
+ Expanded(
+ child: SizedBox(
+ height: 1,
+ child: Container(color: const Color(0xFFCBCBCB)),
+ ),
+ ),
+
+ // Text
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ text,
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ height: 1.193,
+ color: Color(0xFFA0A0A0),
+ ),
+ ),
+ ),
+
+ // Right line
+ Expanded(
+ child: SizedBox(
+ height: 1,
+ child: Container(color: const Color(0xFFCBCBCB)),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/user_id_card.dart b/frontend/lib/widgets/user_id_card.dart
new file mode 100644
index 0000000..6909151
--- /dev/null
+++ b/frontend/lib/widgets/user_id_card.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+
+class UserIDCard extends StatelessWidget {
+ final String userName;
+ final String userId;
+ final double? width;
+
+ const UserIDCard({
+ super.key,
+ required this.userName,
+ required this.userId,
+ this.width,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ constraints: BoxConstraints(maxWidth: width ?? double.infinity),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ '$userName님의 아이디는',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF5C5C5C),
+ height: 1.193,
+ ),
+ ),
+ Text(
+ '$userId입니다.',
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF5C5C5C),
+ height: 1.193,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/lib/widgets/verification_code_input.dart b/frontend/lib/widgets/verification_code_input.dart
new file mode 100644
index 0000000..cf68408
--- /dev/null
+++ b/frontend/lib/widgets/verification_code_input.dart
@@ -0,0 +1,110 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class VerificationCodeInput extends StatefulWidget {
+ final int length;
+ final ValueChanged? onChanged;
+ final double? width;
+ final double? height;
+
+ const VerificationCodeInput({
+ super.key,
+ this.length = 4,
+ this.onChanged,
+ this.width,
+ this.height,
+ });
+
+ @override
+ State createState() => _VerificationCodeInputState();
+}
+
+class _VerificationCodeInputState extends State {
+ final List _controllers = [];
+ final List _focusNodes = [];
+ String _code = '';
+
+ @override
+ void initState() {
+ super.initState();
+ for (int i = 0; i < widget.length; i++) {
+ _controllers.add(TextEditingController());
+ _focusNodes.add(FocusNode());
+ }
+ }
+
+ @override
+ void dispose() {
+ for (var controller in _controllers) {
+ controller.dispose();
+ }
+ for (var focusNode in _focusNodes) {
+ focusNode.dispose();
+ }
+ super.dispose();
+ }
+
+ void _onTextChanged(String value, int index) {
+ if (value.length == 1) {
+ // Move to next field
+ if (index < widget.length - 1) {
+ _focusNodes[index + 1].requestFocus();
+ }
+ } else if (value.isEmpty) {
+ // Move to previous field
+ if (index > 0) {
+ _focusNodes[index - 1].requestFocus();
+ }
+ }
+
+ // Update code
+ _code = _controllers.map((controller) => controller.text).join();
+ widget.onChanged?.call(_code);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: widget.width ?? 248,
+ height: widget.height ?? 50,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: List.generate(widget.length, (index) {
+ return Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ color: _controllers[index].text.isNotEmpty
+ ? Colors.white
+ : const Color(0xFFF3F3F3),
+ border: _controllers[index].text.isNotEmpty
+ ? Border.all(color: const Color(0xFF666EDE), width: 2)
+ : null,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: TextField(
+ controller: _controllers[index],
+ focusNode: _focusNodes[index],
+ textAlign: TextAlign.center,
+ maxLength: 1,
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ style: const TextStyle(
+ fontFamily: 'Pretendard',
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF5C5C5C),
+ ),
+ decoration: const InputDecoration(
+ counterText: '',
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ ),
+ onChanged: (value) => _onTextChanged(value, index),
+ ),
+ );
+ }),
+ ),
+ );
+ }
+}
diff --git a/frontend/linux/.gitignore b/frontend/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/frontend/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/frontend/linux/CMakeLists.txt b/frontend/linux/CMakeLists.txt
new file mode 100644
index 0000000..36b1acd
--- /dev/null
+++ b/frontend/linux/CMakeLists.txt
@@ -0,0 +1,128 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "gradi_frontend")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.gradi.gradi_frontend")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+ set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+ install(FILES "${bundled_library}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/frontend/linux/flutter/CMakeLists.txt b/frontend/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..d5bd016
--- /dev/null
+++ b/frontend/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/frontend/linux/flutter/generated_plugin_registrant.cc b/frontend/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..e71a16d
--- /dev/null
+++ b/frontend/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}
diff --git a/frontend/linux/flutter/generated_plugin_registrant.h b/frontend/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..e0f0a47
--- /dev/null
+++ b/frontend/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/frontend/linux/flutter/generated_plugins.cmake b/frontend/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2e1de87
--- /dev/null
+++ b/frontend/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/frontend/linux/runner/CMakeLists.txt b/frontend/linux/runner/CMakeLists.txt
new file mode 100644
index 0000000..e97dabc
--- /dev/null
+++ b/frontend/linux/runner/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the application ID.
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
diff --git a/frontend/linux/runner/main.cc b/frontend/linux/runner/main.cc
new file mode 100644
index 0000000..e7c5c54
--- /dev/null
+++ b/frontend/linux/runner/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/frontend/linux/runner/my_application.cc b/frontend/linux/runner/my_application.cc
new file mode 100644
index 0000000..c62301d
--- /dev/null
+++ b/frontend/linux/runner/my_application.cc
@@ -0,0 +1,144 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Called when first Flutter frame received.
+static void first_frame_cb(MyApplication* self, FlView *view)
+{
+ gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+}
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "gradi_frontend");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "gradi_frontend");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ GdkRGBA background_color;
+ // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent.
+ gdk_rgba_parse(&background_color, "#000000");
+ fl_view_set_background_color(view, &background_color);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ // Show the window when Flutter renders.
+ // Requires the view to be realized so we can start rendering.
+ g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self);
+ gtk_widget_realize(GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GApplication::startup.
+static void my_application_startup(GApplication* application) {
+ //MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application startup.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
+}
+
+// Implements GApplication::shutdown.
+static void my_application_shutdown(GApplication* application) {
+ //MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application shutdown.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+ G_APPLICATION_CLASS(klass)->startup = my_application_startup;
+ G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ // Set the program name to the application ID, which helps various systems
+ // like GTK and desktop environments map this running application to its
+ // corresponding .desktop file. This ensures better integration by allowing
+ // the application to be recognized beyond its binary name.
+ g_set_prgname(APPLICATION_ID);
+
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_NON_UNIQUE,
+ nullptr));
+}
diff --git a/frontend/linux/runner/my_application.h b/frontend/linux/runner/my_application.h
new file mode 100644
index 0000000..72271d5
--- /dev/null
+++ b/frontend/linux/runner/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/frontend/macos/.gitignore b/frontend/macos/.gitignore
new file mode 100644
index 0000000..746adbb
--- /dev/null
+++ b/frontend/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/frontend/macos/Flutter/Flutter-Debug.xcconfig b/frontend/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/frontend/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/frontend/macos/Flutter/Flutter-Release.xcconfig b/frontend/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/frontend/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000..cccf817
--- /dev/null
+++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,10 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+}
diff --git a/frontend/macos/Runner.xcodeproj/project.pbxproj b/frontend/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..9d551fe
--- /dev/null
+++ b/frontend/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,705 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* gradi_frontend.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gradi_frontend.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 331C80D6294CF71000263BE5 /* RunnerTests */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* gradi_frontend.app */,
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C80D1294CF70F00263BE5 /* Sources */,
+ 331C80D2294CF70F00263BE5 /* Frameworks */,
+ 331C80D3294CF70F00263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* gradi_frontend.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C80D4294CF70F00263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 331C80D4294CF70F00263BE5 /* RunnerTests */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C80D3294CF70F00263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C80D1294CF70F00263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+ };
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 331C80DB294CF71000263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gradi_frontend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gradi_frontend";
+ };
+ name = Debug;
+ };
+ 331C80DC294CF71000263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gradi_frontend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gradi_frontend";
+ };
+ name = Release;
+ };
+ 331C80DD294CF71000263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gradi_frontend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gradi_frontend";
+ };
+ name = Profile;
+ };
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C80DB294CF71000263BE5 /* Debug */,
+ 331C80DC294CF71000263BE5 /* Release */,
+ 331C80DD294CF71000263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/frontend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..d36270f
--- /dev/null
+++ b/frontend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/frontend/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/frontend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/frontend/macos/Runner/AppDelegate.swift b/frontend/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000..b3c1761
--- /dev/null
+++ b/frontend/macos/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Cocoa
+import FlutterMacOS
+
+@main
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a2ec33f
--- /dev/null
+++ b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000..82b6f9d
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000..13b35eb
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000..0a3f5fa
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000..bdb5722
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000..f083318
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000..326c0e7
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000..2f1632c
Binary files /dev/null and b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/frontend/macos/Runner/Base.lproj/MainMenu.xib b/frontend/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000..80e867a
--- /dev/null
+++ b/frontend/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/macos/Runner/Configs/AppInfo.xcconfig b/frontend/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000..c28ba51
--- /dev/null
+++ b/frontend/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = gradi_frontend
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.gradi.gradiFrontend
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2025 com.gradi. All rights reserved.
diff --git a/frontend/macos/Runner/Configs/Debug.xcconfig b/frontend/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000..36b0fd9
--- /dev/null
+++ b/frontend/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/frontend/macos/Runner/Configs/Release.xcconfig b/frontend/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000..dff4f49
--- /dev/null
+++ b/frontend/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/frontend/macos/Runner/Configs/Warnings.xcconfig b/frontend/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000..42bcbf4
--- /dev/null
+++ b/frontend/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/frontend/macos/Runner/DebugProfile.entitlements b/frontend/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000..dddb8a3
--- /dev/null
+++ b/frontend/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/frontend/macos/Runner/Info.plist b/frontend/macos/Runner/Info.plist
new file mode 100644
index 0000000..4789daa
--- /dev/null
+++ b/frontend/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/frontend/macos/Runner/MainFlutterWindow.swift b/frontend/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000..3cc05eb
--- /dev/null
+++ b/frontend/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/frontend/macos/Runner/Release.entitlements b/frontend/macos/Runner/Release.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/frontend/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/frontend/macos/RunnerTests/RunnerTests.swift b/frontend/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..61f3bd1
--- /dev/null
+++ b/frontend/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Cocoa
+import FlutterMacOS
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/frontend/package.json b/frontend/package.json
index daa52e3..e69de29 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,32 +0,0 @@
-{
- "name": "gradi-frontend",
- "version": "1.0.0",
- "description": "Gradi Auto Grading Service - Frontend",
- "main": "index.js",
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "next": "^14.0.0",
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- },
- "devDependencies": {
- "@types/node": "^20.0.0",
- "@types/react": "^18.0.0",
- "@types/react-dom": "^18.0.0",
- "typescript": "^5.0.0",
- "eslint": "^8.0.0",
- "eslint-config-next": "^14.0.0"
- },
- "keywords": [
- "auto-grading",
- "education",
- "nextjs"
- ],
- "author": "Gradi Team",
- "license": "MIT"
-}
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
new file mode 100644
index 0000000..61314e6
--- /dev/null
+++ b/frontend/pubspec.lock
@@ -0,0 +1,309 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.13.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.1"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.3"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ flutter_svg:
+ dependency: "direct main"
+ description:
+ name: flutter_svg
+ sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ http:
+ dependency: "direct main"
+ description:
+ name: http
+ sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.2"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.0.2"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.10"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.17"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.16.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.1"
+ path_parsing:
+ dependency: transitive
+ description:
+ name: path_parsing
+ sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.1"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.12.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.6"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ vector_graphics:
+ dependency: transitive
+ description:
+ name: vector_graphics
+ sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.19"
+ vector_graphics_codec:
+ dependency: transitive
+ description:
+ name: vector_graphics_codec
+ sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.13"
+ vector_graphics_compiler:
+ dependency: transitive
+ description:
+ name: vector_graphics_compiler
+ sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.19"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
+ url: "https://pub.dev"
+ source: hosted
+ version: "15.0.2"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.6.1"
+sdks:
+ dart: ">=3.9.2 <4.0.0"
+ flutter: ">=3.29.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
new file mode 100644
index 0000000..81f7509
--- /dev/null
+++ b/frontend/pubspec.yaml
@@ -0,0 +1,92 @@
+name: gradi_frontend
+description: "A new Flutter project."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: "none" # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.9.2
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.8
+ http: ^1.2.0
+ flutter_svg: ^2.0.10+1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^5.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ assets:
+ - assets/images/
+ - assets/images/icons/
+ - assets/images/bookcovers/
+ - assets/images/social_login/
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/to/resolution-aware-images
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/to/asset-from-package
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/to/font-from-package
diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart
new file mode 100644
index 0000000..0b221cc
--- /dev/null
+++ b/frontend/test/widget_test.dart
@@ -0,0 +1,21 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:gradi_frontend/main.dart';
+
+void main() {
+ testWidgets('GRADI app smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const GradiApp());
+
+ // Verify that the login page loads
+ expect(find.text('GRADI'), findsOneWidget);
+ expect(find.text('로그인'), findsOneWidget);
+ });
+}
diff --git a/frontend/web/favicon.png b/frontend/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/frontend/web/favicon.png differ
diff --git a/frontend/web/icons/Icon-192.png b/frontend/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/frontend/web/icons/Icon-192.png differ
diff --git a/frontend/web/icons/Icon-512.png b/frontend/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/frontend/web/icons/Icon-512.png differ
diff --git a/frontend/web/icons/Icon-maskable-192.png b/frontend/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/frontend/web/icons/Icon-maskable-192.png differ
diff --git a/frontend/web/icons/Icon-maskable-512.png b/frontend/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/frontend/web/icons/Icon-maskable-512.png differ
diff --git a/frontend/web/index.html b/frontend/web/index.html
new file mode 100644
index 0000000..58dbead
--- /dev/null
+++ b/frontend/web/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ gradi_frontend
+
+
+
+
+
+
diff --git a/frontend/web/manifest.json b/frontend/web/manifest.json
new file mode 100644
index 0000000..6c103e6
--- /dev/null
+++ b/frontend/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "gradi_frontend",
+ "short_name": "gradi_frontend",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/frontend/windows/.gitignore b/frontend/windows/.gitignore
new file mode 100644
index 0000000..d492d0d
--- /dev/null
+++ b/frontend/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/frontend/windows/CMakeLists.txt b/frontend/windows/CMakeLists.txt
new file mode 100644
index 0000000..8588a9b
--- /dev/null
+++ b/frontend/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(gradi_frontend LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "gradi_frontend")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/frontend/windows/flutter/CMakeLists.txt b/frontend/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000..903f489
--- /dev/null
+++ b/frontend/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..8b6d468
--- /dev/null
+++ b/frontend/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+}
diff --git a/frontend/windows/flutter/generated_plugin_registrant.h b/frontend/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..dc139d8
--- /dev/null
+++ b/frontend/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..b93c4c3
--- /dev/null
+++ b/frontend/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/frontend/windows/runner/CMakeLists.txt b/frontend/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000..394917c
--- /dev/null
+++ b/frontend/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/frontend/windows/runner/Runner.rc b/frontend/windows/runner/Runner.rc
new file mode 100644
index 0000000..2c7f0dd
--- /dev/null
+++ b/frontend/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.gradi" "\0"
+ VALUE "FileDescription", "gradi_frontend" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "gradi_frontend" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2025 com.gradi. All rights reserved." "\0"
+ VALUE "OriginalFilename", "gradi_frontend.exe" "\0"
+ VALUE "ProductName", "gradi_frontend" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/frontend/windows/runner/flutter_window.cpp b/frontend/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000..955ee30
--- /dev/null
+++ b/frontend/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/frontend/windows/runner/flutter_window.h b/frontend/windows/runner/flutter_window.h
new file mode 100644
index 0000000..6da0652
--- /dev/null
+++ b/frontend/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/frontend/windows/runner/main.cpp b/frontend/windows/runner/main.cpp
new file mode 100644
index 0000000..a45c619
--- /dev/null
+++ b/frontend/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"gradi_frontend", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/frontend/windows/runner/resource.h b/frontend/windows/runner/resource.h
new file mode 100644
index 0000000..66a65d1
--- /dev/null
+++ b/frontend/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/frontend/windows/runner/resources/app_icon.ico b/frontend/windows/runner/resources/app_icon.ico
new file mode 100644
index 0000000..c04e20c
Binary files /dev/null and b/frontend/windows/runner/resources/app_icon.ico differ
diff --git a/frontend/windows/runner/runner.exe.manifest b/frontend/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..153653e
--- /dev/null
+++ b/frontend/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
diff --git a/frontend/windows/runner/utils.cpp b/frontend/windows/runner/utils.cpp
new file mode 100644
index 0000000..3a0b465
--- /dev/null
+++ b/frontend/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ unsigned int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/frontend/windows/runner/utils.h b/frontend/windows/runner/utils.h
new file mode 100644
index 0000000..3879d54
--- /dev/null
+++ b/frontend/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/frontend/windows/runner/win32_window.cpp b/frontend/windows/runner/win32_window.cpp
new file mode 100644
index 0000000..60608d0
--- /dev/null
+++ b/frontend/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+ kGetPreferredBrightnessRegValue,
+ RRF_RT_REG_DWORD, nullptr, &light_mode,
+ &light_mode_size);
+
+ if (result == ERROR_SUCCESS) {
+ BOOL enable_dark_mode = light_mode == 0;
+ DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &enable_dark_mode, sizeof(enable_dark_mode));
+ }
+}
diff --git a/frontend/windows/runner/win32_window.h b/frontend/windows/runner/win32_window.h
new file mode 100644
index 0000000..e901dde
--- /dev/null
+++ b/frontend/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_