Compare commits

..

143 Commits

Author SHA1 Message Date
ByungCheol 98172b3c56 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m43s
2026-06-30 22:09:15 +09:00
ByungCheol 8d659ea956 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m53s
2026-06-30 22:06:04 +09:00
ByungCheol 46a8cb5515 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m53s
2026-06-30 21:56:06 +09:00
ByungCheol 4c846c5bee Merge branch 'dev'
Deploy / deploy (push) Failing after 10m46s
2026-06-30 21:44:14 +09:00
ByungCheol e2d3be29da Merge branch 'dev'
Deploy / deploy (push) Failing after 14m4s
2026-06-30 21:37:26 +09:00
ByungCheol 20f67bbc76 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m42s
2026-06-29 21:36:18 +09:00
ByungCheol 8e7955b5d7 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m6s
2026-06-29 20:25:52 +09:00
ByungCheol 2329da1090 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m28s
2026-06-29 09:33:35 +09:00
ByungCheol 60794733a1 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m0s
2026-06-29 09:25:59 +09:00
ByungCheol b39190baa2 Merge branch 'dev'
Deploy / deploy (push) Successful in 36s
2026-06-29 09:09:03 +09:00
ByungCheol 48c3664136 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m22s
2026-06-29 08:45:40 +09:00
ByungCheol 091b42eb81 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m40s
2026-06-29 08:33:22 +09:00
ByungCheol d5be3c4fcd Merge branch 'dev'
Deploy / deploy (push) Failing after 13m54s
2026-06-29 08:26:05 +09:00
ByungCheol b7d8ede48f Merge branch 'dev'
Deploy / deploy (push) Failing after 15m28s
2026-06-28 20:29:33 +09:00
ByungCheol 6bb6a9727d Merge branch 'dev'
Deploy / deploy (push) Failing after 15m31s
2026-06-28 20:24:28 +09:00
ByungCheol 5534a2438b Merge branch 'dev'
Deploy / deploy (push) Failing after 12m32s
2026-06-28 19:57:30 +09:00
ByungCheol 791aaad1c0 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m19s
2026-06-28 19:20:42 +09:00
ByungCheol 6b8001b4ad Merge branch 'dev'
Deploy / deploy (push) Failing after 11m38s
2026-06-28 18:43:24 +09:00
ByungCheol ddf0074047 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m43s
2026-06-28 18:29:12 +09:00
ByungCheol 27b8ace100 Merge branch 'dev'
Deploy / deploy (push) Successful in 35s
2026-06-28 18:26:18 +09:00
ByungCheol 6cbe1440b3 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m17s
2026-06-28 18:17:13 +09:00
ByungCheol 077b9598ef Merge branch 'dev'
Deploy / deploy (push) Failing after 11m13s
2026-06-28 18:05:45 +09:00
ByungCheol 9c932e7258 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m2s
2026-06-28 17:43:27 +09:00
ByungCheol 98e413b1a1 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m58s
2026-06-28 17:42:02 +09:00
ByungCheol bcc0c4821f Merge branch 'dev'
Deploy / deploy (push) Failing after 12m13s
2026-06-28 17:29:32 +09:00
ByungCheol d56a8c75c3 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m59s
2026-06-28 17:17:01 +09:00
ByungCheol aed3f996fc Merge branch 'dev'
Deploy / deploy (push) Failing after 15m24s
2026-06-28 17:09:37 +09:00
ByungCheol 8cca35151c Merge branch 'dev'
Deploy / deploy (push) Failing after 15m2s
2026-06-28 16:59:27 +09:00
ByungCheol c114e7672b Merge branch 'dev'
Deploy / deploy (push) Successful in 36s
2026-06-28 16:38:08 +09:00
ByungCheol 0b8064e569 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m55s
2026-06-28 16:21:04 +09:00
ByungCheol 5349cf55f4 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m8s
2026-06-28 16:04:42 +09:00
ByungCheol 199b0c84e9 Merge branch 'dev'
Deploy / deploy (push) Successful in 35s
2026-06-28 15:44:42 +09:00
ByungCheol ad219fd0bb Merge branch 'dev'
Deploy / deploy (push) Failing after 12m16s
2026-06-28 15:24:29 +09:00
ByungCheol 3b542cb33d Merge branch 'dev'
Deploy / deploy (push) Failing after 13m43s
2026-06-28 15:13:01 +09:00
ByungCheol f0b5f87656 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m5s
2026-06-28 14:50:26 +09:00
ByungCheol 9be60000d9 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m0s
2026-06-28 14:45:01 +09:00
ByungCheol c2499c382a Merge branch 'dev'
Deploy / deploy (push) Failing after 11m31s
2026-06-28 14:05:14 +09:00
ByungCheol bf40cbb254 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m10s
2026-06-28 13:34:42 +09:00
ByungCheol 3eee749ab9 Merge branch 'dev'
Deploy / deploy (push) Failing after 15m5s
2026-06-28 13:21:39 +09:00
ByungCheol 8733cf1ee0 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m18s
2026-06-28 12:48:45 +09:00
ByungCheol 6125bf9ad8 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m34s
2026-06-28 12:45:29 +09:00
ByungCheol 09a00a1c37 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m15s
2026-06-28 12:36:50 +09:00
ByungCheol 1da98b6618 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m57s
2026-06-28 12:31:55 +09:00
ByungCheol 2d6af690d4 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m35s
2026-06-28 11:08:46 +09:00
ByungCheol 3f809b80bb Merge branch 'dev'
Deploy / deploy (push) Failing after 13m40s
2026-06-28 09:22:51 +09:00
ByungCheol d8ab99d1ec Merge branch 'dev'
Deploy / deploy (push) Failing after 13m50s
CI / build (push) Failing after 15m19s
2026-06-28 09:04:27 +09:00
ByungCheol e64de9c247 Merge branch 'dev'
CI / build (push) Failing after 10m48s
Deploy / deploy (push) Failing after 14m20s
2026-06-28 08:38:55 +09:00
ByungCheol b8260e9860 Merge branch 'dev'
CI / build (push) Successful in 37s
Deploy / deploy (push) Failing after 10m45s
2026-06-28 08:17:41 +09:00
ByungCheol a6829d4157 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m37s
CI / build (push) Failing after 15m5s
2026-06-28 00:44:57 +09:00
ByungCheol a3bdd2e76d Merge branch 'dev'
Deploy / deploy (push) Successful in 35s
CI / build (push) Failing after 12m53s
2026-06-28 00:02:12 +09:00
ByungCheol 4e03bf6393 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m4s
CI / build (push) Failing after 15m32s
2026-06-27 23:19:30 +09:00
ByungCheol 639a50f8bc Merge branch 'dev'
Deploy / deploy (push) Failing after 13m5s
CI / build (push) Failing after 14m33s
2026-06-27 23:00:17 +09:00
ByungCheol 6fe7f8b168 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m58s
CI / build (push) Failing after 12m25s
2026-06-27 22:27:17 +09:00
ByungCheol a86d77b2c5 Merge branch 'dev'
CI / build (push) Failing after 11m24s
Deploy / deploy (push) Failing after 14m56s
2026-06-27 20:48:38 +09:00
ByungCheol d87dd0f56b Merge branch 'dev'
Deploy / deploy (push) Failing after 12m5s
CI / build (push) Failing after 13m36s
2026-06-27 20:16:26 +09:00
ByungCheol 11ab23ed14 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m52s
CI / build (push) Failing after 13m19s
2026-06-27 19:41:42 +09:00
ByungCheol 82b08df826 Merge branch 'dev'
Deploy / deploy (push) Successful in 35s
CI / build (push) Failing after 12m53s
2026-06-27 19:02:09 +09:00
ByungCheol a5c81cc0f2 Merge branch 'dev'
CI / build (push) Failing after 11m57s
Deploy / deploy (push) Failing after 15m29s
2026-06-27 18:03:39 +09:00
ByungCheol 091abd40be Merge branch 'dev'
Deploy / deploy (push) Failing after 13m6s
CI / build (push) Failing after 14m35s
2026-06-27 16:51:30 +09:00
ByungCheol 38484c51a6 Merge branch 'dev'
CI / build (push) Failing after 11m23s
Deploy / deploy (push) Failing after 14m55s
2026-06-27 16:29:42 +09:00
ByungCheol 1c8d9c427c Merge branch 'dev'
Deploy / deploy (push) Failing after 12m51s
CI / build (push) Failing after 14m20s
2026-06-27 16:21:43 +09:00
ByungCheol 383977bcb5 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m6s
CI / build (push) Failing after 14m37s
2026-06-27 16:16:26 +09:00
ByungCheol e455e8b3a8 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m48s
CI / build (push) Failing after 14m16s
2026-06-27 16:06:49 +09:00
ByungCheol 25b5938ab8 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m23s
CI / build (push) Failing after 13m52s
2026-06-27 15:52:11 +09:00
ByungCheol b4b8833fc1 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m16s
CI / build (push) Failing after 14m44s
2026-06-27 15:31:20 +09:00
ByungCheol 02290413b2 Merge branch 'dev'
CI / build (push) Failing after 11m51s
Deploy / deploy (push) Failing after 15m20s
2026-06-27 15:19:14 +09:00
ByungCheol c8c774f060 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m37s
CI / build (push) Failing after 12m5s
2026-06-27 15:08:59 +09:00
ByungCheol 0f56db744c Merge branch 'dev'
Deploy / deploy (push) Successful in 36s
CI / build (push) Failing after 11m54s
2026-06-27 14:59:09 +09:00
ByungCheol b5d6204a2d Merge branch 'dev'
Deploy / deploy (push) Failing after 12m0s
CI / build (push) Failing after 13m32s
2026-06-27 14:50:11 +09:00
ByungCheol 67ac385e26 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m39s
CI / build (push) Failing after 12m8s
2026-06-24 23:03:52 +09:00
ByungCheol 2e48570876 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m58s
CI / build (push) Failing after 13m26s
2026-06-24 21:57:50 +09:00
ByungCheol 40b517ad10 Merge branch 'dev'
CI / build (push) Successful in 43s
Deploy / deploy (push) Failing after 13m2s
2026-06-24 21:31:39 +09:00
ByungCheol f9aca601b6 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m37s
CI / build (push) Failing after 14m10s
2026-06-23 21:02:09 +09:00
ByungCheol 9f8d9bd7b5 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m36s
CI / build (push) Failing after 14m4s
2026-06-22 23:57:19 +09:00
ByungCheol a25fef65f8 Merge branch 'dev'
CI / build (push) Failing after 11m45s
Deploy / deploy (push) Failing after 15m17s
2026-06-22 23:49:39 +09:00
ByungCheol 5abe6cfc09 Merge branch 'dev'
CI / build (push) Failing after 11m45s
Deploy / deploy (push) Failing after 15m17s
2026-06-22 23:39:38 +09:00
ByungCheol d5ea6f048e Merge branch 'dev'
Deploy / deploy (push) Failing after 13m27s
CI / build (push) Failing after 14m56s
2026-06-22 23:16:46 +09:00
ByungCheol 74017243bb Merge branch 'dev'
Deploy / deploy (push) Failing after 11m25s
CI / build (push) Failing after 12m54s
2026-06-22 22:38:22 +09:00
ByungCheol ca6c8be229 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m27s
CI / build (push) Failing after 13m1s
2026-06-22 21:53:22 +09:00
ByungCheol f54db96a30 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m41s
CI / build (push) Failing after 13m12s
2026-06-20 17:13:26 +09:00
ByungCheol 1ae5f7ec69 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m7s
CI / build (push) Failing after 12m34s
2026-06-14 15:53:56 +09:00
ByungCheol 5f99fb3b5c Merge branch 'dev'
Deploy / deploy (push) Failing after 11m7s
CI / build (push) Failing after 12m36s
2026-06-14 15:43:45 +09:00
ByungCheol a56b8265c7 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m57s
CI / build (push) Failing after 13m25s
2026-06-12 22:52:34 +09:00
ByungCheol 2582bbb59b Merge branch 'dev'
Deploy / deploy (push) Failing after 11m39s
CI / build (push) Failing after 13m7s
2026-06-12 22:33:01 +09:00
ByungCheol eaa5c33d2c Merge branch 'dev'
Deploy / deploy (push) Failing after 13m12s
CI / build (push) Failing after 14m46s
2026-06-12 22:06:20 +09:00
ByungCheol 76b86712ef Merge branch 'dev'
Deploy / deploy (push) Successful in 34s
CI / build (push) Failing after 13m41s
2026-06-09 23:12:40 +09:00
ByungCheol 73895edca6 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m55s
CI / build (push) Failing after 13m24s
2026-06-09 22:50:23 +09:00
ByungCheol 162768d030 Merge branch 'dev'
CI / build (push) Failing after 10m43s
Deploy / deploy (push) Failing after 14m17s
2026-06-07 21:05:47 +09:00
ByungCheol 89eca761b5 Merge branch 'dev'
CI / build (push) Failing after 11m29s
Deploy / deploy (push) Failing after 15m2s
2026-06-07 21:00:01 +09:00
ByungCheol bbe6e93130 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m34s
CI / build (push) Failing after 12m0s
2026-06-07 20:49:28 +09:00
ByungCheol bb6621fae5 Merge branch 'dev'
Deploy / deploy (push) Successful in 38s
CI / build (push) Failing after 15m16s
2026-06-07 20:16:14 +09:00
ByungCheol 6b7955e098 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m19s
CI / build (push) Failing after 12m45s
2026-06-07 20:03:44 +09:00
ByungCheol 868f889505 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m3s
CI / build (push) Failing after 14m30s
2026-06-07 19:57:00 +09:00
ByungCheol 5f23f5f0b8 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m43s
CI / build (push) Failing after 14m9s
2026-06-07 19:47:20 +09:00
ByungCheol 20210f6716 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m16s
CI / build (push) Failing after 13m43s
2026-06-07 19:17:44 +09:00
ByungCheol 3862c33690 Merge branch 'dev'
CI / build (push) Failing after 11m44s
Deploy / deploy (push) Failing after 15m18s
2026-06-07 18:14:40 +09:00
ByungCheol 1d9a2a584e Merge branch 'dev'
Deploy / deploy (push) Failing after 11m33s
CI / build (push) Failing after 12m59s
2026-06-07 18:08:23 +09:00
ByungCheol 28182cc324 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m38s
CI / build (push) Failing after 15m4s
2026-06-07 17:56:20 +09:00
ByungCheol 3e1410a522 Merge branch 'dev'
CI / build (push) Failing after 10m40s
Deploy / deploy (push) Failing after 14m14s
2026-06-07 17:50:43 +09:00
ByungCheol 8a9badb01d Merge branch 'dev'
CI / build (push) Successful in 34s
Deploy / deploy (push) Failing after 11m39s
2026-06-07 17:43:18 +09:00
ByungCheol ecd850f468 Merge branch 'dev'
CI / build (push) Failing after 10m58s
Deploy / deploy (push) Failing after 14m32s
2026-06-07 17:30:26 +09:00
ByungCheol 9d15644180 Merge branch 'dev'
CI / build (push) Failing after 11m4s
Deploy / deploy (push) Failing after 14m39s
2026-06-07 17:25:21 +09:00
ByungCheol 3fb0e36413 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m11s
CI / build (push) Failing after 13m41s
2026-06-07 17:17:42 +09:00
ByungCheol 32522ed512 Merge branch 'dev'
CI / build (push) Failing after 11m57s
Deploy / deploy (push) Failing after 15m30s
2026-06-07 16:54:28 +09:00
ByungCheol 33b3e3dc26 Merge branch 'dev'
CI / build (push) Failing after 11m57s
Deploy / deploy (push) Failing after 15m31s
2026-06-07 16:39:28 +09:00
ByungCheol 6253ddef3d Merge branch 'dev'
CI / build (push) Failing after 11m31s
Deploy / deploy (push) Failing after 15m5s
2026-06-07 16:29:54 +09:00
ByungCheol 2cc4bb301f Merge branch 'dev'
Deploy / deploy (push) Failing after 13m54s
CI / build (push) Failing after 15m31s
2026-06-07 16:20:55 +09:00
ByungCheol 33be9a80da Merge branch 'dev'
CI / build (push) Successful in 26s
Deploy / deploy (push) Failing after 11m57s
2026-06-06 17:09:05 +09:00
ByungCheol 981920b38e Merge branch 'dev'
Deploy / deploy (push) Failing after 12m7s
CI / build (push) Failing after 13m27s
2026-06-06 16:42:47 +09:00
ByungCheol f1a5e729f1 Merge branch 'dev'
Deploy / deploy (push) Failing after 10m54s
CI / build (push) Failing after 12m13s
2026-06-06 15:49:07 +09:00
ByungCheol f03062c92f Merge branch 'dev'
Deploy / deploy (push) Successful in 27s
CI / build (push) Failing after 11m24s
2026-06-06 15:29:56 +09:00
ByungCheol 3c5686e8fa Merge branch 'dev'
Deploy / deploy (push) Failing after 10m53s
CI / build (push) Failing after 12m13s
2026-06-06 15:04:15 +09:00
ByungCheol 5604a232fb Merge branch 'dev'
Deploy / deploy (push) Failing after 12m19s
CI / build (push) Failing after 13m39s
2026-06-06 14:27:37 +09:00
ByungCheol 2978ec7c90 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m43s
CI / build (push) Failing after 13m4s
2026-06-06 14:08:30 +09:00
ByungCheol 0f8134eca4 Merge branch 'dev'
CI / build (push) Failing after 11m28s
Deploy / deploy (push) Failing after 15m12s
2026-06-06 00:16:59 +09:00
ByungCheol 214334a01c Merge branch 'dev'
Deploy / deploy (push) Failing after 11m28s
CI / build (push) Failing after 12m45s
2026-06-04 05:02:03 +09:00
ByungCheol 468852d839 Merge branch 'dev'
Deploy / deploy (push) Successful in 20s
CI / build (push) Failing after 11m11s
2026-06-03 19:03:58 +09:00
ByungCheol 3066e3b37e Merge branch 'dev'
Deploy / deploy (push) Failing after 13m45s
CI / build (push) Failing after 14m58s
2026-06-03 18:55:50 +09:00
ByungCheol 488cc79bca Merge branch 'dev'
Deploy / deploy (push) Failing after 12m24s
CI / build (push) Failing after 13m36s
2026-06-03 18:51:44 +09:00
ByungCheol af272f0ffb Merge branch 'dev'
CI / build (push) Failing after 11m0s
Deploy / deploy (push) Failing after 14m46s
2026-06-03 18:29:02 +09:00
ByungCheol da8f64b738 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m1s
CI / build (push) Failing after 13m14s
2026-06-03 18:11:54 +09:00
ByungCheol 6448756aef Merge branch 'dev'
Deploy / deploy (push) Failing after 11m48s
CI / build (push) Failing after 13m1s
2026-06-03 18:07:04 +09:00
ByungCheol e8c5237ed0 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m1s
CI / build (push) Failing after 13m13s
2026-06-03 18:01:54 +09:00
ByungCheol 3f94ca4d2b Merge branch 'dev'
Deploy / deploy (push) Failing after 12m35s
CI / build (push) Failing after 13m48s
2026-06-03 17:56:20 +09:00
ByungCheol c8690b6d62 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m54s
CI / build (push) Failing after 14m7s
2026-06-03 17:44:04 +09:00
ByungCheol 5bf4e9f516 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m32s
CI / build (push) Failing after 12m46s
2026-06-03 17:37:37 +09:00
ByungCheol 0bd0e847c5 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m12s
CI / build (push) Failing after 12m27s
2026-06-03 17:02:37 +09:00
ByungCheol efb1b7d1b3 Merge branch 'dev'
Deploy / deploy (push) Failing after 12m39s
CI / build (push) Failing after 13m54s
2026-06-03 16:50:57 +09:00
ByungCheol 90fedccd84 Merge branch 'dev'
Deploy / deploy (push) Failing after 11m23s
CI / build (push) Failing after 12m38s
2026-06-03 16:42:35 +09:00
ByungCheol 208f87c24f Merge branch 'dev' into main
CI / build (push) Failing after 10m54s
Deploy / deploy (push) Failing after 14m41s
2026-06-01 22:54:43 +09:00
ByungCheol 0feb655327 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m17s
CI / build (push) Failing after 12m30s
2026-06-01 22:37:57 +09:00
ByungCheol 0606f280b4 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 13m30s
CI / build (push) Failing after 14m44s
2026-06-01 22:10:25 +09:00
ByungCheol 05912d8e7e Merge branch 'dev' into main
Deploy / deploy (push) Failing after 12m39s
CI / build (push) Failing after 13m52s
2026-05-31 21:47:05 +09:00
ByungCheol 80e717e281 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m50s
CI / build (push) Failing after 13m3s
2026-05-31 21:32:12 +09:00
ByungCheol f0932cbc0c Merge branch 'dev' into main
Deploy / deploy (push) Failing after 13m2s
CI / build (push) Failing after 14m15s
2026-05-31 20:26:00 +09:00
ByungCheol b9833f6d23 Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m40s
CI / build (push) Failing after 12m53s
2026-05-31 19:52:25 +09:00
ByungCheol f479a35cef Merge branch 'dev' into main
Deploy / deploy (push) Failing after 11m14s
CI / build (push) Failing after 12m30s
2026-05-31 19:13:14 +09:00
ByungCheol ef2e0db4fd Merge branch 'dev' into main
Deploy / deploy (push) Failing after 12m32s
CI / build (push) Failing after 13m44s
2026-05-31 19:02:08 +09:00
ByungCheol a352655662 Merge branch 'dev' into main
Deploy / deploy (push) Has been cancelled
CI / build (push) Failing after 13m42s
2026-05-31 18:46:59 +09:00
ByungCheol fcfb279b16 Merge branch 'dev' into main
CI / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
fix(ci): SSH 개인키 CR 제거
2026-05-31 17:40:43 +09:00
ByungCheol d94513934b Merge branch 'dev' into main
CI / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
ci: 자동 배포 워크플로 + 서버 설정 파일
2026-05-31 15:55:25 +09:00
ByungCheol a255204ad8 Merge branch 'dev' into main
CI / build (push) Failing after 12m44s
ci: Gitea Actions CI/CD 워크플로 추가
2026-05-31 15:48:59 +09:00
ByungCheol 050efaf902 Merge branch 'dev' into main
가계부·게시판 프론트엔드 구현 + Slim Budget 브랜딩/모바일 대응
2026-05-31 15:45:07 +09:00
8 changed files with 154 additions and 412 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ android {
applicationId "kr.sblog.slimbudget" applicationId "kr.sblog.slimbudget"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 3 versionCode 2
versionName "1.0.2" versionName "1.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+39 -47
View File
@@ -52,23 +52,23 @@
--- ---
## 🚧 현재 상태 (2026-06-30) ## 🚧 현재 상태 (블로커)
- 개발자 계정 신원확인 완료 → 앱 생성 완료(`kr.sblog.slimbudget`) - **개인 개발자 계정 신원확인 대기 중** — 앱 생성 버튼 잠김
- ✅ 필수 설정(데이터안전·콘텐츠등급(전체이용가)·앱액세스(둘러보기)·광고없음·비거래자·방침URL) 완료 - 주소 증빙서류 제출 이슈: 한국 서류 이름 마스킹/주소 미표기 → 도시가스/카드/은행창구 등 무마스킹 서류 확보 중
- ✅ 스토어 등록정보·이미지(아이콘512·피처·스크린샷7) 완료 - 승인 소요: 보통 2~3 영업일, 길면 1~2주
-**비공개 테스트 트랙에 v2(AAB) 게시 → 검토 통과**
- ✅ 구글 로그인 SHA-1 이슈 해결(Play 앱서명 SHA-1 등록) → 로그인 정상
-**테스터 12명 opt-in 대기**(업체 의뢰) → 14일 → 프로덕션 신청
- 📌 조직 계정 전환은 보류(사업자등록 → DUNS 필요. 기프트콘 기능 때 함께)
--- ---
## 📋 남은 로드맵 (프로덕션까지) ## 📋 남은 로드맵
1. **테스터 12명 opt-in** (업체 의뢰) — 14일 카운트 시작 조건 1. **신원확인 승인** (대기) → "앱 만들기" 활성화
2. **14일 연속** 12명 유지 → **프로덕션 액세스 신청** (자료: production-access-draft.md) 2. **앱 만들기** — 이름 "돈돼지 가계부", 한국어, 무료
3. **v3 AAB 업로드** — 이번 세션 변경(외화·월별예산·전체화면모달 등) 반영 (14일 카운트 리셋 안 됨) 3. **익명 셋업** (아래 체크리스트)
4. 승인 → **프로덕션 출시** 4. **필수 설정** — 데이터안전·콘텐츠등급·앱액세스(둘러보기 안내)·광고 없음·금융기능 선언·방침 URL (자료: play-store-listing.md)
5. **스토어 등록정보** — 문구 + 아이콘512·피처·스크린샷7
6. **비공개 테스트 트랙** — AAB 업로드 + **테스터 12명 opt-in**
7. **14일 연속** 12명 유지 → **프로덕션 액세스 신청** (자료: production-access-draft.md)
8. 승인 → **프로덕션 출시**
### ⚠️ 폐쇄 테스트 의무 (개인 계정 + 2023-11-13 이후 개설) ### ⚠️ 폐쇄 테스트 의무 (개인 계정 + 2023-11-13 이후 개설)
- **테스터 12명 이상 · 14일 연속** 폐쇄 테스트 후 프로덕션 신청 가능 - **테스터 12명 이상 · 14일 연속** 폐쇄 테스트 후 프로덕션 신청 가능
@@ -85,48 +85,40 @@
--- ---
## ✅ 적용 완료 (전부 웹/소스 배포됨) ## 💡 출시 전 다듬기 후보 (대기 시간 활용, 선택)
신규(빈손) 유저 경험 위주 — 저위험·고효율 순:
1.**빈 상태 → 행동 유도** (완료) — 홈 '시작하기' 카드 + 가계부내역/계좌 빈 상태 CTA. 데이터 0 계정에서만 노출
2.**오프라인·네트워크 오류 배너** (완료) — http net:error 이벤트 + App.vue 상단 고정 배너(오프라인/서버연결실패, 새로고침 버튼)
3.**앱 잠금(PIN)** (완료) — 자체 구현(해시 저장, 기본 OFF), 시작·복귀 시 잠금, 설정 토글+PIN설정 모달, 'PIN 분실→로그아웃' 갇힘 방지. 생체는 추후(네이티브 플러그인 필요)
4.**전역 에러 폴백** (완료) — errorHandler + onErrorCaptured → 흰 화면 대신 '문제 발생 + 새로고침'
5.**안드로이드 뒤로가기 개선** (완료) — 오버레이 닫기 우선 → 화면 뒤로 → 최상위 '한 번 더 누르면 종료' 토스트
6. 첫 실행 온보딩(1번과 중복이라 보류) / Play 인앱 리뷰(플러그인 필요, 출시 후) / 생체인증(출시 후)
**v2(versionCode 2, 1.0.1, 06-29)에 포함 — 비공개 테스트 게시·검토통과** > ⚠️ 앱 잠금은 시작 흐름을 가로채는 기능이라 **실기기 검증 권장**: 설정→앱잠금 ON→PIN 설정→백그라운드 후 복귀→잠금화면 확인→PIN 해제. (현재 설치 APK엔 미반영 — 재빌드·재설치 필요)
- 신규 유저 빈 상태 CTA(홈 '시작하기'·내역·계좌) / 오프라인·네트워크 오류 배너
- 앱 잠금(PIN, 옵트인) + 카메라/갤러리 복귀 오작동 수정
- 전역 에러 폴백(흰 화면 방지) / 안드로이드 뒤로가기 통합(오버레이→뒤로→종료토스트)
- 분류 순서변경 보호('순서변경' 토글)
**v2 이후 추가 — 웹 배포됨, v3 빌드 대기**
- 앱 잠금 **1시간 유예**(잦은 PIN 입력 해소)
- **외화 입/지출** — 통화+환율 자동조회(open.er-api.com 무료)→원화 환산, 원본 보존. `amount`는 환산 원화라 통계·예산 무변경
- **월별 예산 + 전월/다음달 복사** — budget에 year/month 추가, 기존 예산은 현재 월로 1회 이관
- 내역추가 **전체화면 모달**(바깥 닫힘 방지) / 분류 **아코디언**(대분류 삼각형) / 계좌 셀렉트 정리(종류별 안내·이름만)
- 구글 로그인 **Play 서명 SHA-1 등록**(Google Cloud Android OAuth 클라 추가)
--- ---
## 📌 남은 백로그 (출시 후 업데이트) ## 📌 출시 후 업데이트 백로그 (나중에 진행)
1. **앱 업데이트 안내** — 새 버전 알림 + 구글플레이 바로가기 > 첫 출시엔 미포함. 정식 출시 후 업데이트로 진행할 항목.
2. **관리자 신고글 목록 페이지**
3. **가계부 작성 포인트** — 건당 10P, 일 50P 제한
4. **게시판 포인트 정책 개편** — 작성 50P(도배 제외)·키워드필터·추천+5/비추-1(일50)·남글추천+1(일10)/취소-1
5. (선택) 첫 실행 온보딩 / Play 인앱 리뷰 / 생체 인증 / 외화 환율 자동 고도화
6. **조직 계정 전환 + 기프트콘 교환** — 사업자등록→DUNS→앱이전, B2B 기프트콘 매입
### 🤖 AI 기능 (★ 정식 출시 전 적용 희망) 1. **앱 업데이트 안내 메시지** — 새 버전 있을 때 안내 + 구글플레이 바로가기(스토어 페이지로 이동)
- 전제: **LLM API 키**(Claude Haiku 4.5 권장 — 학습 미사용·톤 우수·저렴 / 또는 Gemini 무료쿼터) + **개인정보처리방침·데이터안전 갱신**(제3자 LLM 추가). 백엔드 프록시·키 서버보관·실패 시 폴백. 2. **관리자 신고글 목록 페이지** — 관리자가 게시판 신고된 글 목록을 확인하는 화면
- **AI 입력 보조** (편의성, 1순위) 3. **가계부 작성 포인트** — 가계부 내역 작성 시 건당 10포인트, **일 50포인트 제한**
-**자연어 한 줄 입력** — "어제 스타벅스 5천원" → 폼 자동 채움(기존 parseText를 LLM 기반으로). 음성=기기STT+① 4. **게시판 포인트 정책 개편**
-**영수증 사진 → AI 구조화** — 멀티모달 LLM이 총액·날짜·가맹점·분류 추천(기존 Vision OCR 보강/대체) 1) 게시글 작성 30 → **50포인트**, **도배성 게시물은 적립 제외**
-**자동 분류 추천** — 가맹점/메모→분류 제안 2) 게시글 등록 **키워드 필터링**
- 원칙: AI는 폼 prefill만, **사용자 확인 후 저장** 3) **추천 +5 / 비추 -1** (일일 최대 적립 50포인트)
- **AI 재무 코멘트** — 자산·수입·예상지출 기반 한마디("이러다 파산각" 등). 일1회 캐시·집계수치만 전송. 4) 다른 사용자 글에 **추천 누르면 +1** (일일 10 제한), 추천 **취소 시 -1**
5.**[수정완료] 분류 화면 순서변경 보호** — 기본 drag 잠금 + '순서변경' 토글로만 가능. (v2 포함)
--- 8.**[기능완료] 외화 입/지출 입력** — 통화 선택+환율 자동조회(open.er-api.com 무료)→원화 환산. `account_entry`에 currency/original_amount/exchange_rate 추가, `amount`는 환산 원화 유지(통계·예산 무변경). 백엔드+프론트 배포됨. (테스터 반영은 v3 빌드 필요)
## 🔨 빌드 상태 > **v2 빌드 완료(versionCode 2, versionName 1.0.1, 2026-06-29)** — #5·#6 + 다듬기. 비공개 테스트 업로드 대기.
- **v2** (versionCode 2, 1.0.1): 비공개 테스트 게시·검토통과 > **다음 빌드 v3 후보**: #8 외화 결제 (+ 이후 백로그). 모아서 versionCode 3으로 빌드.
- **v3** (versionCode 3): 위 'v2 이후 추가' 반영 — 빌드 후 비공개 트랙 업로드 (테스터에게 최신본 제공, 14일 카운트 리셋 안 됨) 6.**[버그·수정완료] 앱 잠금이 카메라/갤러리 복귀 시 오작동** — appLock.suppressLock() 추가, 파일선택은 App.vue 전역 리스너, 카메라/공유는 호출부에서 suppress. (코드 배포됨 — 테스터에게 반영하려면 versionCode 2로 AAB 재빌드 업로드 필요)
--- ---
## 다음 세션 재개 지점 ## 다음 세션 재개 지점
**테스터 12명 opt-in 진행상황 확인**(업체 의뢰) → 12명 + 14일 연속 충족되면 **프로덕션 액세스 신청**(`production-access-draft.md`). **신원확인 승인 여부 확인** → 승인됐으면 "앱 만들기 → 익명 셋업 → 필수설정/등록정보 입력" 진행.
v3 AAB를 비공개 트랙에 업로드해 테스터가 최신본을 받게. 대기 중이면 **테스터 12명 Gmail 확보** + **출시 전 다듬기(빈 상태/오프라인)** 진행 권장.
+4 -8
View File
@@ -76,12 +76,8 @@ const realApi = {
}, },
// 예산 (사용자별) // 예산 (사용자별)
budgets({ year, month } = {}) { budgets() {
return http.get('/account/budgets', { params: { year, month } }) return http.get('/account/budgets')
},
// 다른 월 예산을 이 달로 복사(전월 복사 등)
copyBudget({ fromYear, fromMonth, toYear, toMonth }) {
return http.post('/account/budgets/copy', null, { params: { fromYear, fromMonth, toYear, toMonth } })
}, },
budgetStatus({ year, month }) { budgetStatus({ year, month }) {
return http.get('/account/budgets/status', { params: { year, month } }) return http.get('/account/budgets/status', { params: { year, month } })
@@ -95,8 +91,8 @@ const realApi = {
setExpectedIncome({ year, month, expectedIncome }) { setExpectedIncome({ year, month, expectedIncome }) {
return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } }) return http.put('/account/budgets/income', { expectedIncome }, { params: { year, month } })
}, },
createBudget(payload, { year, month } = {}) { createBudget(payload) {
return http.post('/account/budgets', payload, { params: { year, month } }) return http.post('/account/budgets', payload)
}, },
updateBudget(id, payload) { updateBudget(id, payload) {
return http.put(`/account/budgets/${id}`, payload) return http.put(`/account/budgets/${id}`, payload)
+1 -2
View File
@@ -44,8 +44,7 @@ export async function exportBackup() {
safe(accountApi.wallets()), safe(accountApi.wallets()),
safe(accountApi.categories()), safe(accountApi.categories()),
safe(accountApi.recurrings()), safe(accountApi.recurrings()),
// 예산은 월별 — 백업엔 현재 월 예산을 담는다 safe(accountApi.budgets()),
safe(accountApi.budgets({ year: new Date().getFullYear(), month: new Date().getMonth() + 1 })),
safe(accountApi.tags()), safe(accountApi.tags()),
safe(accountApi.quickEntries()), safe(accountApi.quickEntries()),
]) ])
+48 -225
View File
@@ -1,4 +1,4 @@
1<script setup> <script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { accountApi } from '@/api/accountApi' import { accountApi } from '@/api/accountApi'
import { reminders } from '@/native/reminders' import { reminders } from '@/native/reminders'
@@ -127,56 +127,6 @@ const repayTargetIsCard = computed(() => {
const w = wallets.value.find((x) => x.id === form.toWalletId) const w = wallets.value.find((x) => x.id === form.toWalletId)
return !!w && w.type === 'CARD' return !!w && w.type === 'CARD'
}) })
// 금리가 설정된 대출 계좌를 선택한 경우 자동계산 활성
const repayTargetLoanWallet = computed(() => {
const w = wallets.value.find((x) => x.id === form.toWalletId)
return w?.type === 'LOAN' && w.loanRate ? w : null
})
// 납입금액 입력용 (원리금균등만 사용)
const loanPaymentAmount = ref(null)
function monthlyInterestOf(loan) {
return Math.round(Math.abs(loan.balance || 0) * (Number(loan.loanRate) / 100 / 12))
}
// 상환방식별 자동계산: 원금균등·만기일시는 납입금액 입력 불필요
const loanAutoResult = computed(() => {
const loan = repayTargetLoanWallet.value
if (!loan) return null
const interest = monthlyInterestOf(loan)
if (loan.loanMethod === 'BULLET') {
return { interest, principal: 0, total: interest, needsInput: false }
}
if (loan.loanMethod === 'EQUAL_PRINCIPAL' && loan.loanAmount && loan.loanMonths) {
const principal = Math.round(Number(loan.loanAmount) / loan.loanMonths)
return { interest, principal, total: principal + interest, needsInput: false }
}
return { interest: null, principal: null, total: null, needsInput: true }
})
watch(loanAutoResult, (r) => {
if (r && !r.needsInput) {
form.interest = r.interest
form.principal = r.principal
loanPaymentAmount.value = r.total
}
})
function calcLoanRepayment(payment) {
const loan = repayTargetLoanWallet.value
if (!loan || !payment || payment <= 0) return
const interest = monthlyInterestOf(loan)
form.interest = Math.min(interest, payment)
form.principal = Math.max(0, payment - form.interest)
}
watch(loanPaymentAmount, (val) => {
if (loanAutoResult.value?.needsInput && val) calcLoanRepayment(Number(val))
})
watch(() => form.toWalletId, () => {
loanPaymentAmount.value = null
form.principal = null
form.interest = null
})
// 카드 지출일 때만 할부 입력 노출 (2~24개월, 일시불은 빈값) // 카드 지출일 때만 할부 입력 노출 (2~24개월, 일시불은 빈값)
const showInstallment = computed(() => form.type === 'EXPENSE' && form.walletKind === 'CARD') const showInstallment = computed(() => form.type === 'EXPENSE' && form.walletKind === 'CARD')
const installmentMonthly = computed(() => { const installmentMonthly = computed(() => {
@@ -297,8 +247,6 @@ const WALLET_KINDS = [
] ]
const walletsOfKind = computed(() => wallets.value.filter((w) => w.type === form.walletKind)) const walletsOfKind = computed(() => wallets.value.filter((w) => w.type === form.walletKind))
const toWalletsOfKind = computed(() => wallets.value.filter((w) => w.type === form.toWalletKind)) const toWalletsOfKind = computed(() => wallets.value.filter((w) => w.type === form.toWalletKind))
// 선택한 계좌 종류 라벨(계좌/현금/카드/대출/증권) — 셀렉트 안내문에 사용
const walletKindLabel = computed(() => WALLET_KINDS.find((k) => k.value === form.walletKind)?.label || '계좌')
function walletKindOf(id) { function walletKindOf(id) {
const w = wallets.value.find((x) => x.id === id) const w = wallets.value.find((x) => x.id === id)
return w ? w.type : '' return w ? w.type : ''
@@ -326,39 +274,25 @@ async function loadCategories() {
categories.value = [] categories.value = []
} }
} }
// ===== 분류: 대분류 아코디언 → 소분류 선택 ===== // 현재 구분(수입/지출)에 맞는 분류 + 편집 중 현재 값 보존
const categoryMajor = ref('') // 펼쳐진 대분류 이름 // ===== 분류: 대분류 → 소분류 2단 선택 (긴 드롭다운 스크롤 방지) =====
// 현재 구분의 대분류 객체 목록 const categoryMajor = ref('') // 선택된 대분류 이름
const majorOptionsData = computed(() => // 현재 구분의 대분류(부모 없음) 이름 + 편집 중 현재 대분류 보존
categories.value.filter((c) => c.type === form.type && c.parentId == null) const majorOptions = computed(() => {
) const names = categories.value
// 대분류를 4개씩 행으로 묶음 .filter((c) => c.type === form.type && c.parentId == null)
const majorRows = computed(() => { .map((c) => c.name)
const items = majorOptionsData.value if (categoryMajor.value && !names.includes(categoryMajor.value)) names.unshift(categoryMajor.value)
const rows = [] return names
for (let i = 0; i < items.length; i += 4) rows.push(items.slice(i, i + 4)) })
return rows // 선택된 대분류의 소분류 이름 목록 (없으면 대분류 자체가 분류)
const subOptions = computed(() => {
const major = categories.value.find(
(c) => c.type === form.type && c.parentId == null && c.name === categoryMajor.value,
)
if (!major) return []
return categories.value.filter((c) => c.parentId === major.id).map((c) => c.name)
}) })
// 현재 펼쳐진 대분류 객체
const selectedMajorObj = computed(() =>
majorOptionsData.value.find((m) => m.name === categoryMajor.value) ?? null
)
// 특정 대분류의 소분류 목록
function subsByMajor(majorId) {
return categories.value.filter((c) => c.parentId === majorId)
}
// 대분류 클릭: 소분류 있으면 펼침 토글, 없으면 바로 선택
function onMajorClick(major) {
if (categoryMajor.value === major.name) {
categoryMajor.value = ''
form.category = ''
} else {
categoryMajor.value = major.name
if (!subsByMajor(major.id).length) form.category = major.name
else form.category = ''
}
}
// form.category 로부터 대분류 추론 (편집 진입 시) // form.category 로부터 대분류 추론 (편집 진입 시)
function syncCategoryMajor() { function syncCategoryMajor() {
const cur = form.category const cur = form.category
@@ -374,25 +308,19 @@ function syncCategoryMajor() {
categoryMajor.value = cur categoryMajor.value = cur
} }
} }
// 대분류 변경: 소분류 있으면 소분류 선택 대기 / 없으면 대분류가 곧 분류
function onMajorChange() {
form.category = subOptions.value.length ? '' : categoryMajor.value
}
// 필터용 전체 분류명 (중복 제거) // 필터용 전체 분류명 (중복 제거)
const allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))]) const allCategoryNames = computed(() => [...new Set(categories.value.map((c) => c.name))])
// 모달 내 분류 인라인 추가 // 모달 내 분류 인라인 추가
const addingCategory = ref(false) const addingCategory = ref(false)
const addingAsMajor = ref(true) // true=대분류 추가, false=소분류 추가
const newCategoryName = ref('') const newCategoryName = ref('')
const catSubmitting = ref(false) const catSubmitting = ref(false)
function startAddMajorCategory() { function startAddCategory() {
categoryMajor.value = ''
form.category = ''
newCategoryName.value = '' newCategoryName.value = ''
addingAsMajor.value = true
addingCategory.value = true
}
function startAddSubCategory() {
newCategoryName.value = ''
addingAsMajor.value = false
addingCategory.value = true addingCategory.value = true
} }
function cancelAddCategory() { function cancelAddCategory() {
@@ -1141,9 +1069,9 @@ onMounted(async () => {
</div> </div>
</div> </div>
<select v-if="form.walletKind" v-model="form.walletId" :disabled="submitting"> <select v-if="form.walletKind" v-model="form.walletId" :disabled="submitting">
<option value="">{{ form.walletKind === 'CASH' && (form.type === 'INCOME' || form.type === 'EXPENSE') ? '현금 (계좌 미지정)' : (form.type === 'INCOME' || form.type === 'EXPENSE' ? walletKindLabel + ' 선택' : '출금 계좌 선택') }}</option> <option value="">{{ form.walletKind === 'CASH' && (form.type === 'INCOME' || form.type === 'EXPENSE') ? '현금 (계좌 미지정)' : (form.type === 'INCOME' || form.type === 'EXPENSE' ? '계좌/카드 선택' : '출금 계좌 선택') }}</option>
<option v-for="w in walletsOfKind" :key="w.id" :value="w.id"> <option v-for="w in walletsOfKind" :key="w.id" :value="w.id">
{{ w.name || w.issuer }} {{ w.name }}{{ w.issuer ? ` (${w.issuer})` : '' }}
</option> </option>
</select> </select>
</div> </div>
@@ -1169,7 +1097,7 @@ onMounted(async () => {
<select v-if="form.toWalletKind" v-model="form.toWalletId" :disabled="submitting"> <select v-if="form.toWalletKind" v-model="form.toWalletId" :disabled="submitting">
<option value="">입금 계좌 선택</option> <option value="">입금 계좌 선택</option>
<option v-for="w in toWalletsOfKind" :key="w.id" :value="w.id"> <option v-for="w in toWalletsOfKind" :key="w.id" :value="w.id">
{{ w.name || w.issuer }} {{ w.name }}{{ w.issuer ? ` (${w.issuer})` : '' }}
</option> </option>
</select> </select>
</div> </div>
@@ -1181,71 +1109,31 @@ onMounted(async () => {
<select v-model="form.toWalletId" :disabled="submitting"> <select v-model="form.toWalletId" :disabled="submitting">
<option value="">(선택)</option> <option value="">(선택)</option>
<option v-for="w in liabilityWallets" :key="w.id" :value="w.id"> <option v-for="w in liabilityWallets" :key="w.id" :value="w.id">
{{ w.name || w.issuer }} {{ w.name }}{{ w.issuer ? ` (${w.issuer})` : '' }}
</option> </option>
</select> </select>
</label> </label>
<!-- 금리 설정된 대출 계좌: 방식별 자동계산 --> <label>원금<input v-model.number="form.principal" type="number" min="0" placeholder="원 (이체로 기록)" :disabled="submitting" /></label>
<template v-if="repayTargetLoanWallet"> <label>이자<input v-model.number="form.interest" type="number" min="0" placeholder="원 (지출·분류 이자)" :disabled="submitting" /></label>
<!-- 원리금균등: 납입금액 직접 입력 -->
<template v-if="loanAutoResult?.needsInput">
<label>납입금액(원리금균등)
<input v-model.number="loanPaymentAmount" type="number" min="0"
placeholder="이번 달 납입할 총금액" :disabled="submitting" />
</label>
</template>
<!-- 원금균등·만기일시: 자동계산 결과 표시 -->
<template v-else>
<div class="loan-breakdown">
<span class="bd-label">{{ repayTargetLoanWallet.loanMethod === 'BULLET' ? '만기일시' : '원금균등' }} 자동계산</span>
<span>납입 <b>{{ (loanAutoResult?.total || 0).toLocaleString('ko-KR') }}</b></span>
</div>
</template>
<!-- 계산 결과 표시 + 수동 조정 -->
<div v-if="(form.interest ?? null) !== null || (form.principal ?? null) !== null" class="loan-breakdown">
<span>이자 <b>{{ (form.interest || 0).toLocaleString('ko-KR') }}</b></span>
<span>원금 <b>{{ (form.principal || 0).toLocaleString('ko-KR') }}</b></span>
</div>
<label>이자(조정)<input v-model.number="form.interest" type="number" min="0" :disabled="submitting" /></label>
<label>원금(조정)<input v-model.number="form.principal" type="number" min="0" :disabled="submitting" /></label>
</template>
<template v-else>
<label>원금<input v-model.number="form.principal" type="number" min="0" placeholder="원 (이체로 기록)" :disabled="submitting" /></label>
<label>이자<input v-model.number="form.interest" type="number" min="0" placeholder="원 (지출·분류 이자)" :disabled="submitting" /></label>
</template>
<label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label> <label v-if="repayTargetIsCard">연회비<input v-model.number="form.annualFee" type="number" min="0" placeholder="원 (지출·분류 연회비)" :disabled="submitting" /></label>
</template> </template>
<label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류 <label v-if="form.type === 'INCOME' || form.type === 'EXPENSE'">분류
<div v-if="!addingCategory" class="cat-picker"> <div v-if="!addingCategory" class="cat-input cascade">
<!-- 대분류 행별 렌더링: 선택된 대분류가 속한 바로 아래 소분류 삽입 --> <select v-model="categoryMajor" :disabled="submitting" @change="onMajorChange" title="대분류">
<template v-for="(row, rowIdx) in majorRows" :key="rowIdx"> <option value="">대분류 선택</option>
<div class="acc-major-grid"> <option v-for="m in majorOptions" :key="m" :value="m">{{ m }}</option>
<button </select>
v-for="m in row" :key="m.id" <select v-if="subOptions.length" v-model="form.category" :disabled="submitting" title="소분류">
type="button" class="acc-chip major" <option value="">소분류 선택</option>
:class="{ active: categoryMajor === m.name }" <option v-for="s in subOptions" :key="s" :value="s">{{ s }}</option>
:disabled="submitting" </select>
@click="onMajorClick(m)" <button type="button" class="cat-add-btn" :disabled="submitting" @click="startAddCategory">+ 추가</button>
>{{ m.name }}</button>
</div>
<div v-if="selectedMajorObj && row.some(m => m.id === selectedMajorObj.id) && subsByMajor(selectedMajorObj.id).length" class="acc-sub-grid">
<button
v-for="s in subsByMajor(selectedMajorObj.id)" :key="s.id"
type="button" class="acc-chip sub"
:class="{ active: form.category === s.name }"
:disabled="submitting"
@click="form.category = s.name"
>{{ s.name }}</button>
<button type="button" class="acc-chip add-chip sub-add" :disabled="submitting" @click="startAddSubCategory">+ 소분류</button>
</div>
</template>
<button type="button" class="acc-chip add-chip" :disabled="submitting" @click="startAddMajorCategory">+ 대분류</button>
</div> </div>
<div v-else class="cat-input"> <div v-else class="cat-input">
<input <input
v-model="newCategoryName" type="text" v-model="newCategoryName" type="text"
:placeholder="addingAsMajor ? (form.type === 'EXPENSE' ? '새 지출 분류' : '새 수입 분류') : `${categoryMajor} > 새 소분류`" :placeholder="form.type === 'EXPENSE' ? '새 지출 분류' : '새 수입 분류'"
:disabled="catSubmitting" @keyup.enter.prevent="confirmAddCategory" :disabled="catSubmitting" @keyup.enter.prevent="confirmAddCategory"
/> />
<button type="button" class="cat-add-btn primary" :disabled="catSubmitting" @click="confirmAddCategory">확인</button> <button type="button" class="cat-add-btn primary" :disabled="catSubmitting" @click="confirmAddCategory">확인</button>
@@ -1981,6 +1869,13 @@ button.primary {
display: flex; display: flex;
gap: 0.4rem; gap: 0.4rem;
} }
.cat-input.cascade {
flex-wrap: wrap;
}
.cat-input.cascade select {
flex: 1 1 40%;
}
.cat-input select,
.cat-input input { .cat-input input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@@ -1995,78 +1890,6 @@ button.primary {
border-color: hsla(160, 100%, 37%, 1); border-color: hsla(160, 100%, 37%, 1);
color: hsla(160, 100%, 37%, 1); color: hsla(160, 100%, 37%, 1);
} }
/* 분류 칩 그리드 */
.cat-picker {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.acc-major-grid,
.acc-sub-grid {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.acc-sub-grid {
padding: 0.4rem;
border-radius: 6px;
background: var(--color-background-soft);
}
.acc-chip {
flex: 1 1 calc(25% - 0.35rem);
min-width: 0;
padding: 0.38rem 0.4rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: transparent;
cursor: pointer;
font-size: 0.82rem;
color: var(--color-text);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.acc-chip.major.active {
border-color: hsla(160, 100%, 37%, 1);
background: hsla(160, 100%, 37%, 0.1);
color: hsla(160, 100%, 37%, 1);
font-weight: 600;
}
.acc-chip.sub {
background: var(--color-background);
}
.acc-chip.sub.active {
border-color: hsla(160, 100%, 37%, 1);
background: hsla(160, 100%, 37%, 0.1);
color: hsla(160, 100%, 37%, 1);
font-weight: 600;
}
.acc-chip.add-chip {
opacity: 0.55;
border-style: dashed;
}
.acc-chip.add-chip.sub-add {
flex: 0 0 auto;
}
.loan-breakdown {
display: flex;
gap: 1rem;
padding: 0.45rem 0.6rem;
border-radius: 6px;
background: var(--color-background-soft);
font-size: 0.85rem;
color: var(--color-text);
}
.loan-breakdown b {
color: hsla(160, 100%, 37%, 1);
}
.loan-breakdown .bd-label {
width: 100%;
font-size: 0.78rem;
opacity: 0.6;
margin-bottom: 0.1rem;
}
.tag-field { .tag-field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+1 -57
View File
@@ -33,12 +33,6 @@ const form = reactive({
openingBalance: 0, openingBalance: 0,
openingDate: '', openingDate: '',
currentValue: null, currentValue: null,
// 대출(LOAN) 전용
loanAmount: null,
loanRate: null,
loanMethod: '',
loanMonths: null,
loanStart: '',
}) })
const submitting = ref(false) const submitting = ref(false)
const formError = ref(null) const formError = ref(null)
@@ -202,7 +196,7 @@ function issuerLabel(t) {
return t === 'BANK' ? '은행명' : t === 'CARD' ? '카드사' : t === 'INVEST' ? '증권사' : '대출기관' return t === 'BANK' ? '은행명' : t === 'CARD' ? '카드사' : t === 'INVEST' ? '증권사' : '대출기관'
} }
function openingLabel(t) { function openingLabel(t) {
return t === 'BANK' || t === 'CASH' ? '초기 잔액' : t === 'CARD' ? '초기 미결제 금액' : t === 'INVEST' ? '투자금(투입원금)' : '기록 시작 시 잔액' return t === 'BANK' || t === 'CASH' ? '초기 잔액' : t === 'CARD' ? '초기 미결제 금액' : t === 'INVEST' ? '투자금(투입원금)' : '대출 잔액(원금)'
} }
// 투자 수익률(%) — 투입원금 대비 평가손익 // 투자 수익률(%) — 투입원금 대비 평가손익
function returnPct(w) { function returnPct(w) {
@@ -235,11 +229,6 @@ function openCreate() {
openingBalance: 0, openingBalance: 0,
openingDate: '', openingDate: '',
currentValue: null, currentValue: null,
loanAmount: null,
loanRate: null,
loanMethod: '',
loanMonths: null,
loanStart: '',
}) })
formError.value = null formError.value = null
formOpen.value = true formOpen.value = true
@@ -256,11 +245,6 @@ function openEdit(w) {
openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0, openingBalance: isLiability(w.type) ? -(w.openingBalance || 0) : w.openingBalance || 0,
openingDate: w.openingDate || '', openingDate: w.openingDate || '',
currentValue: w.currentValue ?? null, currentValue: w.currentValue ?? null,
loanAmount: w.loanAmount ?? null,
loanRate: w.loanRate ?? null,
loanMethod: w.loanMethod || '',
loanMonths: w.loanMonths ?? null,
loanStart: w.loanStart || '',
}) })
formError.value = null formError.value = null
formOpen.value = true formOpen.value = true
@@ -274,7 +258,6 @@ async function submit() {
} }
submitting.value = true submitting.value = true
const magnitude = Number(form.openingBalance) || 0 const magnitude = Number(form.openingBalance) || 0
const isLoan = form.type === 'LOAN'
const payload = { const payload = {
type: form.type, type: form.type,
name: form.name.trim(), name: form.name.trim(),
@@ -288,11 +271,6 @@ async function submit() {
form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null form.type === 'INVEST' && form.currentValue !== '' && form.currentValue != null
? Number(form.currentValue) ? Number(form.currentValue)
: null, : null,
loanAmount: isLoan && form.loanAmount ? Number(form.loanAmount) : null,
loanRate: isLoan && form.loanRate !== null && form.loanRate !== '' ? Number(form.loanRate) : null,
loanMethod: isLoan && form.loanMethod ? form.loanMethod : null,
loanMonths: isLoan && form.loanMonths ? Number(form.loanMonths) : null,
loanStart: isLoan && form.loanStart ? form.loanStart : null,
} }
try { try {
if (editId.value) await accountApi.updateWallet(editId.value, payload) if (editId.value) await accountApi.updateWallet(editId.value, payload)
@@ -319,9 +297,6 @@ async function remove(w) {
function cardTypeLabel(v) { function cardTypeLabel(v) {
return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : '' return v === 'CHECK' ? '체크' : v === 'CREDIT' ? '신용' : ''
} }
function loanMethodLabel(v) {
return v === 'EQUAL_PAYMENT' ? '원리금균등' : v === 'EQUAL_PRINCIPAL' ? '원금균등' : v === 'BULLET' ? '만기일시' : ''
}
onMounted(async () => { onMounted(async () => {
await load() await load()
@@ -387,10 +362,6 @@ onBeforeUnmount(() => sortable?.destroy())
>{{ revealedAccts.has(w.id) ? '🙈' : '👁' }}</button> >{{ revealedAccts.has(w.id) ? '🙈' : '👁' }}</button>
</template> </template>
<template v-if="w.type === 'INVEST'"> · 투자금 {{ won(w.investedAmount) }}</template> <template v-if="w.type === 'INVEST'"> · 투자금 {{ won(w.investedAmount) }}</template>
<template v-if="w.type === 'LOAN'">
<span v-if="w.loanAmount"> · 실행 {{ won(w.loanAmount) }}</span>
<span v-if="w.loanRate"> · {{ w.loanRate }}%<span v-if="w.loanMethod"> / {{ loanMethodLabel(w.loanMethod) }}</span></span>
</template>
</span> </span>
</div> </div>
<div class="balance-wrap"> <div class="balance-wrap">
@@ -487,33 +458,6 @@ onBeforeUnmount(() => sortable?.destroy())
</template> </template>
<label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label> <label>기준일<input v-model="form.openingDate" type="date" :disabled="submitting" /></label>
<!-- 대출 전용 -->
<template v-if="form.type === 'LOAN'">
<label>대출 실행 금액(원금)
<input v-model.number="form.loanAmount" type="number" min="0"
placeholder="처음 빌린 금액 (예: 10000000)" :disabled="submitting" />
</label>
<label>연이자율(%)
<input v-model.number="form.loanRate" type="number" min="0" max="100" step="0.01"
placeholder="예: 5.25" :disabled="submitting" />
</label>
<label>상환방식
<select v-model="form.loanMethod" :disabled="submitting">
<option value="">(선택 )</option>
<option value="EQUAL_PAYMENT">원리금균등상환</option>
<option value="EQUAL_PRINCIPAL">원금균등상환</option>
<option value="BULLET">만기일시상환</option>
</select>
</label>
<label>대출기간(개월)
<input v-model.number="form.loanMonths" type="number" min="1"
placeholder="예: 120 (10년)" :disabled="submitting" />
</label>
<label>대출시작일
<input v-model="form.loanStart" type="date" :disabled="submitting" />
</label>
</template>
<p v-if="formError" class="msg error">{{ formError }}</p> <p v-if="formError" class="msg error">{{ formError }}</p>
<div class="buttons"> <div class="buttons">
+2 -68
View File
@@ -127,7 +127,7 @@ async function load() {
error.value = null error.value = null
try { try {
const params = { year: year.value, month: month.value } const params = { year: year.value, month: month.value }
const [st, raw] = await Promise.all([accountApi.budgetStatus(params), accountApi.budgets(params)]) const [st, raw] = await Promise.all([accountApi.budgetStatus(params), accountApi.budgets()])
statuses.value = st statuses.value = st
budgetsRaw.value = raw budgetsRaw.value = raw
} catch (e) { } catch (e) {
@@ -154,46 +154,6 @@ function nextMonth() {
loadIncome() loadIncome()
} }
// ===== 월별 예산 복사 =====
const copying = ref(false)
const ym = (y, m) => `${y}.${String(m).padStart(2, '0')}`
async function copyToNextMonth() {
if (copying.value || !budgetsRaw.value.length) return
const to = month.value === 12 ? { y: year.value + 1, m: 1 } : { y: year.value, m: month.value + 1 }
const ok = await dialog.confirm(
`${ym(year.value, month.value)} 예산을 ${ym(to.y, to.m)}(다음 달)로 복사할까요?\n다음 달의 같은 분류 예산은 덮어씁니다.`,
{ title: '다음 달로 복사' },
)
if (!ok) return
copying.value = true
try {
await accountApi.copyBudget({ fromYear: year.value, fromMonth: month.value, toYear: to.y, toMonth: to.m })
await dialog.alert(`${ym(to.y, to.m)}로 예산을 복사했어요.`)
} catch (e) {
error.value = e.response?.data?.message || '복사에 실패했습니다.'
} finally {
copying.value = false
}
}
async function copyFromPrevMonth() {
if (copying.value) return
const prev = month.value === 1 ? { y: year.value - 1, m: 12 } : { y: year.value, m: month.value - 1 }
const ok = await dialog.confirm(
`전월(${ym(prev.y, prev.m)}) 예산을 ${ym(year.value, month.value)}로 가져올까요?\n이 달의 같은 분류 예산은 덮어씁니다.`,
{ title: '전월 예산 복사' },
)
if (!ok) return
copying.value = true
try {
await accountApi.copyBudget({ fromYear: prev.y, fromMonth: prev.m, toYear: year.value, toMonth: month.value })
await load()
} catch (e) {
error.value = e.response?.data?.message || '복사에 실패했습니다.'
} finally {
copying.value = false
}
}
function openCreate() { function openCreate() {
editId.value = null editId.value = null
Object.assign(form, { Object.assign(form, {
@@ -253,7 +213,7 @@ async function submit() {
submitting.value = true submitting.value = true
try { try {
if (editId.value) await accountApi.updateBudget(editId.value, payload) if (editId.value) await accountApi.updateBudget(editId.value, payload)
else await accountApi.createBudget(payload, { year: year.value, month: month.value }) else await accountApi.createBudget(payload)
formOpen.value = false formOpen.value = false
await load() await load()
} catch (e) { } catch (e) {
@@ -292,12 +252,6 @@ onMounted(() => {
</div> </div>
</div> </div>
<!-- 월별 예산 복사 -->
<div class="copy-actions">
<button type="button" class="copy-btn" :disabled="copying" @click="copyFromPrevMonth"> 전월 예산 가져오기</button>
<button type="button" class="copy-btn" :disabled="copying || !budgetsRaw.length" @click="copyToNextMonth">다음 달로 복사 </button>
</div>
<!-- 예상 수입 vs 예산 --> <!-- 예상 수입 vs 예산 -->
<div class="income-panel"> <div class="income-panel">
<div class="inc-input"> <div class="inc-input">
@@ -477,26 +431,6 @@ button.primary {
text-align: center; text-align: center;
} }
/* 예상 수입 vs 예산 */ /* 예상 수입 vs 예산 */
.copy-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
flex-wrap: wrap;
margin: -0.4rem 0 1rem;
}
.copy-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-background-soft);
color: var(--color-text);
font-size: 0.82rem;
cursor: pointer;
}
.copy-btn:disabled {
opacity: 0.45;
cursor: default;
}
.income-panel { .income-panel {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
+57 -3
View File
@@ -66,6 +66,17 @@ async function reload() {
const majorListEl = ref(null) const majorListEl = ref(null)
const reorderMode = ref(false) const reorderMode = ref(false)
// ===== 대분류 펼침(아코디언) — 삼각형 누르면 소분류 표시 =====
const expanded = ref(new Set())
function toggleExpand(id) {
const s = new Set(expanded.value)
if (s.has(id)) s.delete(id)
else s.add(id)
expanded.value = s
}
function isExpanded(id) {
return expanded.value.has(id)
}
let sortables = [] let sortables = []
function destroySortables() { function destroySortables() {
sortables.forEach((s) => s.destroy()) sortables.forEach((s) => s.destroy())
@@ -129,9 +140,16 @@ async function addCategory() {
if (!name) return if (!name) return
error.value = null error.value = null
try { try {
await accountApi.createCategory({ type: activeType.value, name, parentId: newParent.value || null }) const parentId = newParent.value || null
await accountApi.createCategory({ type: activeType.value, name, parentId })
newName.value = '' newName.value = ''
await reload() await reload()
// 소분류를 추가했으면 그 대분류를 펼쳐 바로 보이게
if (parentId) {
const s = new Set(expanded.value)
s.add(Number(parentId))
expanded.value = s
}
} catch (e) { } catch (e) {
showError(e, '추가에 실패했습니다.') showError(e, '추가에 실패했습니다.')
} }
@@ -227,12 +245,18 @@ onBeforeUnmount(destroySortables)
<div class="cat-row major-row"> <div class="cat-row major-row">
<div class="cat-main"> <div class="cat-main">
<span v-show="reorderMode" class="drag-handle major-handle" title="대분류 순서 변경(소분류 함께 이동)"></span> <span v-show="reorderMode" class="drag-handle major-handle" title="대분류 순서 변경(소분류 함께 이동)"></span>
<button
type="button" class="expand-btn" :class="{ open: isExpanded(g.major.id), empty: !g.children.length }"
:title="g.children.length ? (isExpanded(g.major.id) ? '소분류 접기' : '소분류 펼치기') : '소분류 없음'"
:disabled="!g.children.length" @click="toggleExpand(g.major.id)"
></button>
<input v-model="g.major.name" class="cat-name" @keyup.enter="saveCategory(g.major)" /> <input v-model="g.major.name" class="cat-name" @keyup.enter="saveCategory(g.major)" />
<span v-if="g.children.length && !isExpanded(g.major.id)" class="sub-count">{{ g.children.length }}</span>
<IconBtn icon="check" title="저장" size="sm" @click="saveCategory(g.major)" /> <IconBtn icon="check" title="저장" size="sm" @click="saveCategory(g.major)" />
<IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="removeCategory(g.major)" /> <IconBtn icon="trash" title="삭제" variant="danger" size="sm" @click="removeCategory(g.major)" />
</div> </div>
</div> </div>
<ul class="sub-list" :data-parent="g.major.id"> <ul v-show="isExpanded(g.major.id) || reorderMode" class="sub-list" :data-parent="g.major.id">
<li v-for="s in g.children" :key="s.id" :data-id="s.id" class="sub-item cat-row child"> <li v-for="s in g.children" :key="s.id" :data-id="s.id" class="sub-item cat-row child">
<div class="cat-main"> <div class="cat-main">
<span v-show="reorderMode" class="drag-handle sub-handle" title="소분류 순서 변경"></span> <span v-show="reorderMode" class="drag-handle sub-handle" title="소분류 순서 변경"></span>
@@ -444,7 +468,37 @@ button.danger {
.sub-mark { .sub-mark {
opacity: 0.45; opacity: 0.45;
} }
/* 대분류 펼침 삼각형 */
.expand-btn {
flex: none;
width: 1.4rem;
height: 1.4rem;
border: 0;
background: transparent;
color: var(--color-text);
font-size: 0.7rem;
cursor: pointer;
transition: transform 0.15s ease;
opacity: 0.7;
}
.expand-btn.open {
transform: rotate(90deg);
}
.expand-btn.empty {
opacity: 0.15;
cursor: default;
}
.sub-count {
flex: none;
min-width: 1.2rem;
text-align: center;
font-size: 0.72rem;
padding: 0.05rem 0.3rem;
border-radius: 999px;
background: var(--color-border);
color: var(--color-text);
opacity: 0.75;
}
.hint.sm { .hint.sm {
font-size: 0.78rem; font-size: 0.78rem;
margin: 0 0 0.75rem; margin: 0 0 0.75rem;