diff --git a/.git-rewrite/backup-refs b/.git-rewrite/backup-refs new file mode 100644 index 00000000..de3fbbb9 --- /dev/null +++ b/.git-rewrite/backup-refs @@ -0,0 +1,19 @@ +0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 commit refs/heads/develop +3ac4011c72d33fa7fc26e7873fb52d91048d2b7e commit refs/heads/feat/10/user +55f0e49d95aa04487ed94d548ffdd44d0ded8a06 commit refs/heads/feat/20/home +2968ce57e4274710c986e005c93997ce503eb895 commit refs/heads/feat/26/collectionDetail +52fd2147e9bf5905213d955934ff0ab565b4d777 commit refs/heads/main +52fd2147e9bf5905213d955934ff0ab565b4d777 commit refs/remotes/origin/HEAD +7acd794e3027d816efe7a4f797ef1c2ad9332a4c commit refs/remotes/origin/bug/11/idea-파일-추적-문제-해결 +e8750da3afbbf333be3c92b06eb0d45cbabae584 commit refs/remotes/origin/deploy +c724ddb7d6061686a25d8bd381f5555809d2f089 commit refs/remotes/origin/develop +d28c519f6a0e6d21c11339d8f9ea99e347f5d63f commit refs/remotes/origin/feat/10/user +528c6626c4be064722b39c9a65cea01df5031c55 commit refs/remotes/origin/feat/14/memo-api-개발 +55f0e49d95aa04487ed94d548ffdd44d0ded8a06 commit refs/remotes/origin/feat/20/home +dc16f64202b085b8af210eb9d26fbccea3117eac commit refs/remotes/origin/feat/25/재생-목록-api-개발임베드-기능-구현 +d49669cc6e54195a81161465953f29137bd116b2 commit refs/remotes/origin/feat/26/collectionDetail +54c0bd75730eafd65d49141c4aa49f5f58ab38a2 commit refs/remotes/origin/feat/26/s3연결 +923cce8cb09c5583bbc7cd242e39f3a9b9afc6dc commit refs/remotes/origin/feat/29/myPage +4978d556b292ad2f4abc5437ffb096e0120becca commit refs/remotes/origin/feat/43/email +52fd2147e9bf5905213d955934ff0ab565b4d777 commit refs/remotes/origin/main +3215d5a7d47df633d1b61bc319df088935f55c00 commit refs/remotes/origin/revert-36-develop diff --git a/.git-rewrite/commit b/.git-rewrite/commit new file mode 100644 index 00000000..69917637 --- /dev/null +++ b/.git-rewrite/commit @@ -0,0 +1,6 @@ +tree bc3c4629562ce1fa051e717abbf4820dff1a579b +parent e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 +author LJYeon12 1737374474 +0900 +committer LJYeon12 1737374474 +0900 + +apiResponse 응답 수정 제외 완료 diff --git a/.git-rewrite/heads b/.git-rewrite/heads new file mode 100644 index 00000000..e7aa58f6 --- /dev/null +++ b/.git-rewrite/heads @@ -0,0 +1,19 @@ +refs/heads/develop +refs/heads/feat/10/user +refs/heads/feat/20/home +refs/heads/feat/26/collectionDetail +refs/heads/main +refs/remotes/origin/main +refs/remotes/origin/bug/11/idea-파일-추적-문제-해결 +refs/remotes/origin/deploy +refs/remotes/origin/develop +refs/remotes/origin/feat/10/user +refs/remotes/origin/feat/14/memo-api-개발 +refs/remotes/origin/feat/20/home +refs/remotes/origin/feat/25/재생-목록-api-개발임베드-기능-구현 +refs/remotes/origin/feat/26/collectionDetail +refs/remotes/origin/feat/26/s3연결 +refs/remotes/origin/feat/29/myPage +refs/remotes/origin/feat/43/email +refs/remotes/origin/main +refs/remotes/origin/revert-36-develop diff --git a/.git-rewrite/index b/.git-rewrite/index new file mode 100644 index 00000000..27a3db01 Binary files /dev/null and b/.git-rewrite/index differ diff --git a/.git-rewrite/map/00d077ebd580a3c15cb8839f5031dbc38a7f3959 b/.git-rewrite/map/00d077ebd580a3c15cb8839f5031dbc38a7f3959 new file mode 100644 index 00000000..d0ae927c --- /dev/null +++ b/.git-rewrite/map/00d077ebd580a3c15cb8839f5031dbc38a7f3959 @@ -0,0 +1 @@ +fffd6d612002020cbb0cf0634b5666e8c41c9658 diff --git a/.git-rewrite/map/024743bf4a56f00680faf2929f124c114a611be4 b/.git-rewrite/map/024743bf4a56f00680faf2929f124c114a611be4 new file mode 100644 index 00000000..14d40ed9 --- /dev/null +++ b/.git-rewrite/map/024743bf4a56f00680faf2929f124c114a611be4 @@ -0,0 +1 @@ +30c9d45b7e500b5dcae1d5428a341e610d3226aa diff --git a/.git-rewrite/map/0a4d5316489ceb617515710013c84b154b704e25 b/.git-rewrite/map/0a4d5316489ceb617515710013c84b154b704e25 new file mode 100644 index 00000000..f5fa6c05 --- /dev/null +++ b/.git-rewrite/map/0a4d5316489ceb617515710013c84b154b704e25 @@ -0,0 +1 @@ +ce9244162dc0336a8349833bc9def91e9ff2d372 diff --git a/.git-rewrite/map/0aed3109ef2251f403ff3f9573522ebb265fd672 b/.git-rewrite/map/0aed3109ef2251f403ff3f9573522ebb265fd672 new file mode 100644 index 00000000..cd3a1891 --- /dev/null +++ b/.git-rewrite/map/0aed3109ef2251f403ff3f9573522ebb265fd672 @@ -0,0 +1 @@ +3cae279b868e45d1beb32e3eacccb4fb2359ad2a diff --git a/.git-rewrite/map/1027d3692020a63d227e6013df1672632bd3d26a b/.git-rewrite/map/1027d3692020a63d227e6013df1672632bd3d26a new file mode 100644 index 00000000..603b83d5 --- /dev/null +++ b/.git-rewrite/map/1027d3692020a63d227e6013df1672632bd3d26a @@ -0,0 +1 @@ +981bdbd6e640c9029ccd469885a1cb8a80bb992d diff --git a/.git-rewrite/map/115206189608cf37e4cad16622617f47f4b74027 b/.git-rewrite/map/115206189608cf37e4cad16622617f47f4b74027 new file mode 100644 index 00000000..e92bdef0 --- /dev/null +++ b/.git-rewrite/map/115206189608cf37e4cad16622617f47f4b74027 @@ -0,0 +1 @@ +0cdf7c046964cdaf1de60077885bfcf948dd6f19 diff --git a/.git-rewrite/map/1256997c50b1a350d832b00706200827711b1566 b/.git-rewrite/map/1256997c50b1a350d832b00706200827711b1566 new file mode 100644 index 00000000..1790d96a --- /dev/null +++ b/.git-rewrite/map/1256997c50b1a350d832b00706200827711b1566 @@ -0,0 +1 @@ +95da63a04fdc4cc97b245b05a611e5e608b076a6 diff --git a/.git-rewrite/map/1604df6e567b12148948b7b4009eb80b9ccff25e b/.git-rewrite/map/1604df6e567b12148948b7b4009eb80b9ccff25e new file mode 100644 index 00000000..10c531f4 --- /dev/null +++ b/.git-rewrite/map/1604df6e567b12148948b7b4009eb80b9ccff25e @@ -0,0 +1 @@ +be3d8683b6c3f5e6525b66c4fa2e375fa2af2716 diff --git a/.git-rewrite/map/18a914c828173fdfb84fdb9666bfb7485b730941 b/.git-rewrite/map/18a914c828173fdfb84fdb9666bfb7485b730941 new file mode 100644 index 00000000..29ddf36d --- /dev/null +++ b/.git-rewrite/map/18a914c828173fdfb84fdb9666bfb7485b730941 @@ -0,0 +1 @@ +a7237fcd3771d283b124cbdda5fdb3c3c7294b4a diff --git a/.git-rewrite/map/1b9e0695764bca055decf36ff0a61507e5f0974f b/.git-rewrite/map/1b9e0695764bca055decf36ff0a61507e5f0974f new file mode 100644 index 00000000..2b9ce18d --- /dev/null +++ b/.git-rewrite/map/1b9e0695764bca055decf36ff0a61507e5f0974f @@ -0,0 +1 @@ +fb5e1eff9ed400fa90350304b75ac15a4ce80717 diff --git a/.git-rewrite/map/1df55aa5e4a964744f1ff121271371cbd4dfcc33 b/.git-rewrite/map/1df55aa5e4a964744f1ff121271371cbd4dfcc33 new file mode 100644 index 00000000..01fec742 --- /dev/null +++ b/.git-rewrite/map/1df55aa5e4a964744f1ff121271371cbd4dfcc33 @@ -0,0 +1 @@ +ac5771bf63033913919def62c189f496abd40a6f diff --git a/.git-rewrite/map/225595e2f112d03031bd65fa7e28a9b82df89750 b/.git-rewrite/map/225595e2f112d03031bd65fa7e28a9b82df89750 new file mode 100644 index 00000000..f7a6bfa1 --- /dev/null +++ b/.git-rewrite/map/225595e2f112d03031bd65fa7e28a9b82df89750 @@ -0,0 +1 @@ +f20b493f074a9ff8382bdc71ed9b10f60518a106 diff --git a/.git-rewrite/map/28e284d206a8db02b9e23d6ba972a0b0cbdd16ec b/.git-rewrite/map/28e284d206a8db02b9e23d6ba972a0b0cbdd16ec new file mode 100644 index 00000000..d22aaa80 --- /dev/null +++ b/.git-rewrite/map/28e284d206a8db02b9e23d6ba972a0b0cbdd16ec @@ -0,0 +1 @@ +9ab0bafd57c5064147678574cb2f892cc55d8359 diff --git a/.git-rewrite/map/2a39a83bd1692c1bdaaa568e6931cc3c5d49b193 b/.git-rewrite/map/2a39a83bd1692c1bdaaa568e6931cc3c5d49b193 new file mode 100644 index 00000000..57a356b3 --- /dev/null +++ b/.git-rewrite/map/2a39a83bd1692c1bdaaa568e6931cc3c5d49b193 @@ -0,0 +1 @@ +b968e8f4eabb869cb085cc7079f8fc0f4af9c590 diff --git a/.git-rewrite/map/2a39d7a9d8da3a355e1a56539c7dc62881e253b2 b/.git-rewrite/map/2a39d7a9d8da3a355e1a56539c7dc62881e253b2 new file mode 100644 index 00000000..82a8e36d --- /dev/null +++ b/.git-rewrite/map/2a39d7a9d8da3a355e1a56539c7dc62881e253b2 @@ -0,0 +1 @@ +09025f9e3b347ffefa2420bcd57580b0f484640e diff --git a/.git-rewrite/map/2bc5b20d61ab922707471dc835c94c5bd4267ad4 b/.git-rewrite/map/2bc5b20d61ab922707471dc835c94c5bd4267ad4 new file mode 100644 index 00000000..55aa48fa --- /dev/null +++ b/.git-rewrite/map/2bc5b20d61ab922707471dc835c94c5bd4267ad4 @@ -0,0 +1 @@ +f601c422bed956194e6922f42058343dc180fab8 diff --git a/.git-rewrite/map/2c22217d1127dec9ae39dd84b0570b5bbeea3b99 b/.git-rewrite/map/2c22217d1127dec9ae39dd84b0570b5bbeea3b99 new file mode 100644 index 00000000..ceba971e --- /dev/null +++ b/.git-rewrite/map/2c22217d1127dec9ae39dd84b0570b5bbeea3b99 @@ -0,0 +1 @@ +9f9eeef747eebd8417b48f4d04466ded59c506d1 diff --git a/.git-rewrite/map/2d68a12033ad43f6c109dcd3d704a8449cbefea5 b/.git-rewrite/map/2d68a12033ad43f6c109dcd3d704a8449cbefea5 new file mode 100644 index 00000000..f9f733a9 --- /dev/null +++ b/.git-rewrite/map/2d68a12033ad43f6c109dcd3d704a8449cbefea5 @@ -0,0 +1 @@ +1d661b03a3f0927044999e7747356af88cfc06ee diff --git a/.git-rewrite/map/2d6e57418118f19fc236e0efe2ae05b9ce747216 b/.git-rewrite/map/2d6e57418118f19fc236e0efe2ae05b9ce747216 new file mode 100644 index 00000000..7707171c --- /dev/null +++ b/.git-rewrite/map/2d6e57418118f19fc236e0efe2ae05b9ce747216 @@ -0,0 +1 @@ +98a3a03fa672547edfdcca5da22f12de36aed899 diff --git a/.git-rewrite/map/2e0357eb2ac4928ded47a80359f6cb3a4cf11dd8 b/.git-rewrite/map/2e0357eb2ac4928ded47a80359f6cb3a4cf11dd8 new file mode 100644 index 00000000..9d356ee8 --- /dev/null +++ b/.git-rewrite/map/2e0357eb2ac4928ded47a80359f6cb3a4cf11dd8 @@ -0,0 +1 @@ +24efde0a103e0a4a852ab33088657430f0df308b diff --git a/.git-rewrite/map/2f23763e398d690722dd1fd3886d5b33baf8d37a b/.git-rewrite/map/2f23763e398d690722dd1fd3886d5b33baf8d37a new file mode 100644 index 00000000..98e0c02a --- /dev/null +++ b/.git-rewrite/map/2f23763e398d690722dd1fd3886d5b33baf8d37a @@ -0,0 +1 @@ +706524ee6aaadae4b0a327fcd0ac32ede8ae2747 diff --git a/.git-rewrite/map/31187961b7a877d99681ecccc92bb11c662489cb b/.git-rewrite/map/31187961b7a877d99681ecccc92bb11c662489cb new file mode 100644 index 00000000..88eb778c --- /dev/null +++ b/.git-rewrite/map/31187961b7a877d99681ecccc92bb11c662489cb @@ -0,0 +1 @@ +b208e4e9b82eab64fb20e5b2e82231794f7f2293 diff --git a/.git-rewrite/map/32267c564f63e143a1bcc06831257fd37909fd99 b/.git-rewrite/map/32267c564f63e143a1bcc06831257fd37909fd99 new file mode 100644 index 00000000..52038590 --- /dev/null +++ b/.git-rewrite/map/32267c564f63e143a1bcc06831257fd37909fd99 @@ -0,0 +1 @@ +2252084a34d4cbab737aafe47bcd01c9e1037fc6 diff --git a/.git-rewrite/map/32beed45a4405c3a3e2543140d7c635be272d704 b/.git-rewrite/map/32beed45a4405c3a3e2543140d7c635be272d704 new file mode 100644 index 00000000..4ac03168 --- /dev/null +++ b/.git-rewrite/map/32beed45a4405c3a3e2543140d7c635be272d704 @@ -0,0 +1 @@ +bc2a8ef6d1cc5de45254a6262e5449dea1931498 diff --git a/.git-rewrite/map/34cf04b4b2c695417cea61eabe647102a24cc1f1 b/.git-rewrite/map/34cf04b4b2c695417cea61eabe647102a24cc1f1 new file mode 100644 index 00000000..1d544a53 --- /dev/null +++ b/.git-rewrite/map/34cf04b4b2c695417cea61eabe647102a24cc1f1 @@ -0,0 +1 @@ +898985b4da24b916fd96ecd3c54ff67c94802790 diff --git a/.git-rewrite/map/3731e945fbabc5389f4841316a4ba0ed5aa8a37c b/.git-rewrite/map/3731e945fbabc5389f4841316a4ba0ed5aa8a37c new file mode 100644 index 00000000..c24be855 --- /dev/null +++ b/.git-rewrite/map/3731e945fbabc5389f4841316a4ba0ed5aa8a37c @@ -0,0 +1 @@ +611ce8d0b121e070c394605782051f1569106a1b diff --git a/.git-rewrite/map/375274c762f5854e75de7920c5bc426ba4bf86e4 b/.git-rewrite/map/375274c762f5854e75de7920c5bc426ba4bf86e4 new file mode 100644 index 00000000..2df7b157 --- /dev/null +++ b/.git-rewrite/map/375274c762f5854e75de7920c5bc426ba4bf86e4 @@ -0,0 +1 @@ +59becfa4d3338e9fad9ddc37ab5854c0d01e8bb0 diff --git a/.git-rewrite/map/3d01e511aec0e6f38b08f6bfe293df28eadc0ef0 b/.git-rewrite/map/3d01e511aec0e6f38b08f6bfe293df28eadc0ef0 new file mode 100644 index 00000000..c75cfab5 --- /dev/null +++ b/.git-rewrite/map/3d01e511aec0e6f38b08f6bfe293df28eadc0ef0 @@ -0,0 +1 @@ +c4614f40950bfc5e72ba080198152b34c50332b0 diff --git a/.git-rewrite/map/4a41a86bfa78852f8912c7ef5eb55f1651424000 b/.git-rewrite/map/4a41a86bfa78852f8912c7ef5eb55f1651424000 new file mode 100644 index 00000000..79fe4bbb --- /dev/null +++ b/.git-rewrite/map/4a41a86bfa78852f8912c7ef5eb55f1651424000 @@ -0,0 +1 @@ +e1083de07e707438b4d5c01b40e2e2df206eb7dc diff --git a/.git-rewrite/map/4c0e52ba25fe177c30cbbd970a3c491aae6efd73 b/.git-rewrite/map/4c0e52ba25fe177c30cbbd970a3c491aae6efd73 new file mode 100644 index 00000000..12a52d6c --- /dev/null +++ b/.git-rewrite/map/4c0e52ba25fe177c30cbbd970a3c491aae6efd73 @@ -0,0 +1 @@ +ee1a30962e6f97687755c258d4ced139d37e769a diff --git a/.git-rewrite/map/4f1e828bc3607019ae1f13217f147f82ffee0936 b/.git-rewrite/map/4f1e828bc3607019ae1f13217f147f82ffee0936 new file mode 100644 index 00000000..14298eb7 --- /dev/null +++ b/.git-rewrite/map/4f1e828bc3607019ae1f13217f147f82ffee0936 @@ -0,0 +1 @@ +fe1d6d44aa0a1f11669f57cc7eb148874e5cfc69 diff --git a/.git-rewrite/map/4fb06b940a58d067f13f920bb25b3df8d33f48a1 b/.git-rewrite/map/4fb06b940a58d067f13f920bb25b3df8d33f48a1 new file mode 100644 index 00000000..8a20240e --- /dev/null +++ b/.git-rewrite/map/4fb06b940a58d067f13f920bb25b3df8d33f48a1 @@ -0,0 +1 @@ +f86656291bbf9dc6f4485d969d5f0ba02c52ff94 diff --git a/.git-rewrite/map/50b9a3a2a303c7e8089586ba98ee59898c268ffb b/.git-rewrite/map/50b9a3a2a303c7e8089586ba98ee59898c268ffb new file mode 100644 index 00000000..cee41077 --- /dev/null +++ b/.git-rewrite/map/50b9a3a2a303c7e8089586ba98ee59898c268ffb @@ -0,0 +1 @@ +8b58ecf51ffdd39059a4c276920513942ff8a21a diff --git a/.git-rewrite/map/512da0db748012e1e19995ec91d71ef04de9d43f b/.git-rewrite/map/512da0db748012e1e19995ec91d71ef04de9d43f new file mode 100644 index 00000000..5041b884 --- /dev/null +++ b/.git-rewrite/map/512da0db748012e1e19995ec91d71ef04de9d43f @@ -0,0 +1 @@ +57eea61c715c733013d44e2b7db6c693aa09cf4b diff --git a/.git-rewrite/map/57cfa1165c473e237f49f157b15c3a50d2b47d30 b/.git-rewrite/map/57cfa1165c473e237f49f157b15c3a50d2b47d30 new file mode 100644 index 00000000..b0785cfe --- /dev/null +++ b/.git-rewrite/map/57cfa1165c473e237f49f157b15c3a50d2b47d30 @@ -0,0 +1 @@ +5d965c69c12ec8fc73a9c6acaad1a658617273fa diff --git a/.git-rewrite/map/588420029ecebb4fedea59d91a92a158cb2907e9 b/.git-rewrite/map/588420029ecebb4fedea59d91a92a158cb2907e9 new file mode 100644 index 00000000..ef4c27fd --- /dev/null +++ b/.git-rewrite/map/588420029ecebb4fedea59d91a92a158cb2907e9 @@ -0,0 +1 @@ +5dd8f1a53c26afaf348a760dd3cdd3508ecc1626 diff --git a/.git-rewrite/map/59b3909e1d5b553ede74b5c5afabc03dac35b784 b/.git-rewrite/map/59b3909e1d5b553ede74b5c5afabc03dac35b784 new file mode 100644 index 00000000..3772bb48 --- /dev/null +++ b/.git-rewrite/map/59b3909e1d5b553ede74b5c5afabc03dac35b784 @@ -0,0 +1 @@ +4babe95eaf1a3c389bc3b93967232ee390d34d01 diff --git a/.git-rewrite/map/5b9d161239f8f55a1efa96fd8f9df8185b9c7231 b/.git-rewrite/map/5b9d161239f8f55a1efa96fd8f9df8185b9c7231 new file mode 100644 index 00000000..484611da --- /dev/null +++ b/.git-rewrite/map/5b9d161239f8f55a1efa96fd8f9df8185b9c7231 @@ -0,0 +1 @@ +321380f73f5eacd2d967e56f6c8ed86c3912579d diff --git a/.git-rewrite/map/5fdd4033af5d0aa47c072b59a1524fff46e2eeb5 b/.git-rewrite/map/5fdd4033af5d0aa47c072b59a1524fff46e2eeb5 new file mode 100644 index 00000000..84927da5 --- /dev/null +++ b/.git-rewrite/map/5fdd4033af5d0aa47c072b59a1524fff46e2eeb5 @@ -0,0 +1 @@ +ee834dfedc5326a56bb7a753d096011569e18e9c diff --git a/.git-rewrite/map/61f7fc386faf3f5fb1f9a5b5aa4a4138bbd8595c b/.git-rewrite/map/61f7fc386faf3f5fb1f9a5b5aa4a4138bbd8595c new file mode 100644 index 00000000..d1cb189e --- /dev/null +++ b/.git-rewrite/map/61f7fc386faf3f5fb1f9a5b5aa4a4138bbd8595c @@ -0,0 +1 @@ +3889b7171aaa385fc36eaf6f1de28248f9c3acbd diff --git a/.git-rewrite/map/6550e626ddc9bed759ae80efd9ff4b39f275c5d4 b/.git-rewrite/map/6550e626ddc9bed759ae80efd9ff4b39f275c5d4 new file mode 100644 index 00000000..9fce1848 --- /dev/null +++ b/.git-rewrite/map/6550e626ddc9bed759ae80efd9ff4b39f275c5d4 @@ -0,0 +1 @@ +cbd5377b83612c267cd6e44d89ce8cc1d713fd33 diff --git a/.git-rewrite/map/6746d022b3db99e56467434c692d91dfd1f33784 b/.git-rewrite/map/6746d022b3db99e56467434c692d91dfd1f33784 new file mode 100644 index 00000000..17ac5dc5 --- /dev/null +++ b/.git-rewrite/map/6746d022b3db99e56467434c692d91dfd1f33784 @@ -0,0 +1 @@ +4d33b2b673e5b00f996a12cda75246887ea8cec1 diff --git a/.git-rewrite/map/68f0b732c5bafa9772b9a177c0abb0e926d1f4c6 b/.git-rewrite/map/68f0b732c5bafa9772b9a177c0abb0e926d1f4c6 new file mode 100644 index 00000000..4b03ac82 --- /dev/null +++ b/.git-rewrite/map/68f0b732c5bafa9772b9a177c0abb0e926d1f4c6 @@ -0,0 +1 @@ +bc8fcfaaf58d9965d18455df1f80b998ada4772e diff --git a/.git-rewrite/map/690adbdfc477591814576166884379457b299afe b/.git-rewrite/map/690adbdfc477591814576166884379457b299afe new file mode 100644 index 00000000..77442539 --- /dev/null +++ b/.git-rewrite/map/690adbdfc477591814576166884379457b299afe @@ -0,0 +1 @@ +e82cd8640facc38cde5583a0306356de8601af7b diff --git a/.git-rewrite/map/6aa7a296f0014a7f728aea69bb464b76a5b50bf1 b/.git-rewrite/map/6aa7a296f0014a7f728aea69bb464b76a5b50bf1 new file mode 100644 index 00000000..a2ab34a3 --- /dev/null +++ b/.git-rewrite/map/6aa7a296f0014a7f728aea69bb464b76a5b50bf1 @@ -0,0 +1 @@ +ae883c923ad1c283f6ef3c31d7bf076e9020028c diff --git a/.git-rewrite/map/6ad520067aa82cf9a29af4cc9d9caf755c56f7f4 b/.git-rewrite/map/6ad520067aa82cf9a29af4cc9d9caf755c56f7f4 new file mode 100644 index 00000000..28c6bbc6 --- /dev/null +++ b/.git-rewrite/map/6ad520067aa82cf9a29af4cc9d9caf755c56f7f4 @@ -0,0 +1 @@ +ba4248f0a32b2cb79b050c79df21fc6d23f6c348 diff --git a/.git-rewrite/map/6be417102c1307c44ff33802a33cd0e11933dbb0 b/.git-rewrite/map/6be417102c1307c44ff33802a33cd0e11933dbb0 new file mode 100644 index 00000000..67af4ba8 --- /dev/null +++ b/.git-rewrite/map/6be417102c1307c44ff33802a33cd0e11933dbb0 @@ -0,0 +1 @@ +3046ad70d3f97fb9d1cd894fde462a9a43005b72 diff --git a/.git-rewrite/map/6d1e3a61c094f511eec2d4240d98e85590e77c67 b/.git-rewrite/map/6d1e3a61c094f511eec2d4240d98e85590e77c67 new file mode 100644 index 00000000..fcd9282d --- /dev/null +++ b/.git-rewrite/map/6d1e3a61c094f511eec2d4240d98e85590e77c67 @@ -0,0 +1 @@ +304c7d2a5d524d99fda8b112c0f205ffe061580c diff --git a/.git-rewrite/map/6e2ce634458b0ea3133a5cb9c5c7c19983ca7206 b/.git-rewrite/map/6e2ce634458b0ea3133a5cb9c5c7c19983ca7206 new file mode 100644 index 00000000..97ebfd45 --- /dev/null +++ b/.git-rewrite/map/6e2ce634458b0ea3133a5cb9c5c7c19983ca7206 @@ -0,0 +1 @@ +3ea34a894d02c5b75156db4da67b8827d54304d0 diff --git a/.git-rewrite/map/747246a0b46d742b5a9087a823a435ec88464d8b b/.git-rewrite/map/747246a0b46d742b5a9087a823a435ec88464d8b new file mode 100644 index 00000000..3f32975a --- /dev/null +++ b/.git-rewrite/map/747246a0b46d742b5a9087a823a435ec88464d8b @@ -0,0 +1 @@ +3dc802b105ab609206d8dd5b7c22977242be6d85 diff --git a/.git-rewrite/map/7647f7f62d9d14463358e323ef229c68fae8d4f7 b/.git-rewrite/map/7647f7f62d9d14463358e323ef229c68fae8d4f7 new file mode 100644 index 00000000..bf71e33f --- /dev/null +++ b/.git-rewrite/map/7647f7f62d9d14463358e323ef229c68fae8d4f7 @@ -0,0 +1 @@ +fd483c85ffe421d1e7e389b55364a768d6fc3bbb diff --git a/.git-rewrite/map/79bd2b0ca55bb0de73723bff874c3880d7c46c1a b/.git-rewrite/map/79bd2b0ca55bb0de73723bff874c3880d7c46c1a new file mode 100644 index 00000000..fe837956 --- /dev/null +++ b/.git-rewrite/map/79bd2b0ca55bb0de73723bff874c3880d7c46c1a @@ -0,0 +1 @@ +1b23496420cfb235b20d96bdc65aa99ac4f35aee diff --git a/.git-rewrite/map/7acd794e3027d816efe7a4f797ef1c2ad9332a4c b/.git-rewrite/map/7acd794e3027d816efe7a4f797ef1c2ad9332a4c new file mode 100644 index 00000000..75e3081e --- /dev/null +++ b/.git-rewrite/map/7acd794e3027d816efe7a4f797ef1c2ad9332a4c @@ -0,0 +1 @@ +832918332616809fe47d8ac164a54db81c75aa2d diff --git a/.git-rewrite/map/7fea266ee2de59125833d31cd2e73f15a2521847 b/.git-rewrite/map/7fea266ee2de59125833d31cd2e73f15a2521847 new file mode 100644 index 00000000..cabb7c9c --- /dev/null +++ b/.git-rewrite/map/7fea266ee2de59125833d31cd2e73f15a2521847 @@ -0,0 +1 @@ +da2fe3047c519869e1339c67acb5501aef8000c0 diff --git a/.git-rewrite/map/819a798a995d665c70cb78fe530357b54872adba b/.git-rewrite/map/819a798a995d665c70cb78fe530357b54872adba new file mode 100644 index 00000000..0c8e4ee2 --- /dev/null +++ b/.git-rewrite/map/819a798a995d665c70cb78fe530357b54872adba @@ -0,0 +1 @@ +3dea948be7a5e819d56b83d33d0a5419a701dda6 diff --git a/.git-rewrite/map/8851a91a4c942a18b1570f88e070571cf4e8d240 b/.git-rewrite/map/8851a91a4c942a18b1570f88e070571cf4e8d240 new file mode 100644 index 00000000..87581062 --- /dev/null +++ b/.git-rewrite/map/8851a91a4c942a18b1570f88e070571cf4e8d240 @@ -0,0 +1 @@ +97a321398a37cbd28f35a9f9edb290fd4a0a20cb diff --git a/.git-rewrite/map/89fe5fc6e826f2a85add24554798bcb688f6ea49 b/.git-rewrite/map/89fe5fc6e826f2a85add24554798bcb688f6ea49 new file mode 100644 index 00000000..3621a3ec --- /dev/null +++ b/.git-rewrite/map/89fe5fc6e826f2a85add24554798bcb688f6ea49 @@ -0,0 +1 @@ +c730a0cf709f23d7b3a691cf3cda0bcbf2ab44a2 diff --git a/.git-rewrite/map/8a587f92e0e299c443f37d07561c541870ac0f61 b/.git-rewrite/map/8a587f92e0e299c443f37d07561c541870ac0f61 new file mode 100644 index 00000000..72966973 --- /dev/null +++ b/.git-rewrite/map/8a587f92e0e299c443f37d07561c541870ac0f61 @@ -0,0 +1 @@ +1410c6ad9b859a119bc1d90147b5dd125b8c9f57 diff --git a/.git-rewrite/map/8a65b930ce38cce80e4af36572230cf9270e24f0 b/.git-rewrite/map/8a65b930ce38cce80e4af36572230cf9270e24f0 new file mode 100644 index 00000000..2ecb2a2b --- /dev/null +++ b/.git-rewrite/map/8a65b930ce38cce80e4af36572230cf9270e24f0 @@ -0,0 +1 @@ +2f81f4155f82184cf1af35e4f8c657753b628182 diff --git a/.git-rewrite/map/8a73db471e5d5ffb03f188c37733cd47a0b1a44d b/.git-rewrite/map/8a73db471e5d5ffb03f188c37733cd47a0b1a44d new file mode 100644 index 00000000..2ad24497 --- /dev/null +++ b/.git-rewrite/map/8a73db471e5d5ffb03f188c37733cd47a0b1a44d @@ -0,0 +1 @@ +294d003f418781dc56c6f2b41059b6e37653bd70 diff --git a/.git-rewrite/map/8bf259f60bf69a59739b518ef688bb28d5c82c0d b/.git-rewrite/map/8bf259f60bf69a59739b518ef688bb28d5c82c0d new file mode 100644 index 00000000..3e84b7ad --- /dev/null +++ b/.git-rewrite/map/8bf259f60bf69a59739b518ef688bb28d5c82c0d @@ -0,0 +1 @@ +6fa4142ed3bc13638117234e0910e895b725413e diff --git a/.git-rewrite/map/8bf5e39020a808f2a077cfcc83aebaefb4956608 b/.git-rewrite/map/8bf5e39020a808f2a077cfcc83aebaefb4956608 new file mode 100644 index 00000000..c5d3994e --- /dev/null +++ b/.git-rewrite/map/8bf5e39020a808f2a077cfcc83aebaefb4956608 @@ -0,0 +1 @@ +70c9f5fe2c424686075e52e30f84893b8d9f0f63 diff --git a/.git-rewrite/map/8ce9ceb1c57de4b08667c9221aadb30203abcae9 b/.git-rewrite/map/8ce9ceb1c57de4b08667c9221aadb30203abcae9 new file mode 100644 index 00000000..16997840 --- /dev/null +++ b/.git-rewrite/map/8ce9ceb1c57de4b08667c9221aadb30203abcae9 @@ -0,0 +1 @@ +79588aa51beb0059946acba80de8eeddfd457763 diff --git a/.git-rewrite/map/8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce b/.git-rewrite/map/8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce new file mode 100644 index 00000000..9a7cc75e --- /dev/null +++ b/.git-rewrite/map/8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce @@ -0,0 +1 @@ +5c72d7c0d17e51967152e334bad401ddeb9d9a1d diff --git a/.git-rewrite/map/91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 b/.git-rewrite/map/91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 new file mode 100644 index 00000000..00ff7e9b --- /dev/null +++ b/.git-rewrite/map/91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 @@ -0,0 +1 @@ +fb09a4dbab576520ac1ef60c8816ed9bbb443241 diff --git a/.git-rewrite/map/920d0fa6dd901f0d1e74b94b3edbd441a2e090a1 b/.git-rewrite/map/920d0fa6dd901f0d1e74b94b3edbd441a2e090a1 new file mode 100644 index 00000000..efb60196 --- /dev/null +++ b/.git-rewrite/map/920d0fa6dd901f0d1e74b94b3edbd441a2e090a1 @@ -0,0 +1 @@ +25ee3dd28e509b6778b5aa76689c8305b6af50f3 diff --git a/.git-rewrite/map/922edc9106a8d35f712985588ea234178016b267 b/.git-rewrite/map/922edc9106a8d35f712985588ea234178016b267 new file mode 100644 index 00000000..72acbde6 --- /dev/null +++ b/.git-rewrite/map/922edc9106a8d35f712985588ea234178016b267 @@ -0,0 +1 @@ +23cdda984d00be2acd279d3f94ac77db18151d7b diff --git a/.git-rewrite/map/92306d9ae13c2a67fa32a9cb1ae9408845f9d218 b/.git-rewrite/map/92306d9ae13c2a67fa32a9cb1ae9408845f9d218 new file mode 100644 index 00000000..2d304daf --- /dev/null +++ b/.git-rewrite/map/92306d9ae13c2a67fa32a9cb1ae9408845f9d218 @@ -0,0 +1 @@ +bbd946650d69d2bad8ea38ad3ce1f1341c6f06de diff --git a/.git-rewrite/map/964df6c74d43a4014cb0b5d79ca1dc69e95f2b78 b/.git-rewrite/map/964df6c74d43a4014cb0b5d79ca1dc69e95f2b78 new file mode 100644 index 00000000..a6089fd6 --- /dev/null +++ b/.git-rewrite/map/964df6c74d43a4014cb0b5d79ca1dc69e95f2b78 @@ -0,0 +1 @@ +556a1c63fd82fe2fe105190bb4467ad9ad3f4702 diff --git a/.git-rewrite/map/975af36bcc11682c94324e3b7c85902e3206e793 b/.git-rewrite/map/975af36bcc11682c94324e3b7c85902e3206e793 new file mode 100644 index 00000000..d0235ecf --- /dev/null +++ b/.git-rewrite/map/975af36bcc11682c94324e3b7c85902e3206e793 @@ -0,0 +1 @@ +096b2aa3bdfe5256ba4a39c80e34c4046597e4e5 diff --git a/.git-rewrite/map/975fb67b21d0e4091444bd9d5637e4dc3db4ab53 b/.git-rewrite/map/975fb67b21d0e4091444bd9d5637e4dc3db4ab53 new file mode 100644 index 00000000..ffc0fe8c --- /dev/null +++ b/.git-rewrite/map/975fb67b21d0e4091444bd9d5637e4dc3db4ab53 @@ -0,0 +1 @@ +a8d54c2af200172594e37b6bd5664d222e8cb740 diff --git a/.git-rewrite/map/992c59133f0d5ed139567d522f6eb05ffac4fe4a b/.git-rewrite/map/992c59133f0d5ed139567d522f6eb05ffac4fe4a new file mode 100644 index 00000000..807fa55f --- /dev/null +++ b/.git-rewrite/map/992c59133f0d5ed139567d522f6eb05ffac4fe4a @@ -0,0 +1 @@ +3a5923eae976eed2c44a06a73a5cbc5b9e819c08 diff --git a/.git-rewrite/map/996e5a7bb14aa3719ceac2b3fa62ce334c5aaba8 b/.git-rewrite/map/996e5a7bb14aa3719ceac2b3fa62ce334c5aaba8 new file mode 100644 index 00000000..cadd6337 --- /dev/null +++ b/.git-rewrite/map/996e5a7bb14aa3719ceac2b3fa62ce334c5aaba8 @@ -0,0 +1 @@ +4709099d1cdda0d63e5190e0532bb8e256454ca3 diff --git a/.git-rewrite/map/9b2f00c1ca8a6e70f24fd44f23fc289a3738f162 b/.git-rewrite/map/9b2f00c1ca8a6e70f24fd44f23fc289a3738f162 new file mode 100644 index 00000000..bfcb9e0d --- /dev/null +++ b/.git-rewrite/map/9b2f00c1ca8a6e70f24fd44f23fc289a3738f162 @@ -0,0 +1 @@ +ecced3c2a2a4d521d5e6f97f8e593716b1372d16 diff --git a/.git-rewrite/map/9f2c9293a69f195a7c26519fbee8aa09fcf70c40 b/.git-rewrite/map/9f2c9293a69f195a7c26519fbee8aa09fcf70c40 new file mode 100644 index 00000000..3aa0b07d --- /dev/null +++ b/.git-rewrite/map/9f2c9293a69f195a7c26519fbee8aa09fcf70c40 @@ -0,0 +1 @@ +f09343e9cd0228de3f6fa256e7dfb9e8fd6691f9 diff --git a/.git-rewrite/map/a048d733cedb1a6780b8f77b134969c1b7f7df7c b/.git-rewrite/map/a048d733cedb1a6780b8f77b134969c1b7f7df7c new file mode 100644 index 00000000..eb475117 --- /dev/null +++ b/.git-rewrite/map/a048d733cedb1a6780b8f77b134969c1b7f7df7c @@ -0,0 +1 @@ +457e683503e241e38bf780938f6b9495df5c6d6f diff --git a/.git-rewrite/map/a29f6e6968735a388d7964726ad121b3de3d8541 b/.git-rewrite/map/a29f6e6968735a388d7964726ad121b3de3d8541 new file mode 100644 index 00000000..1f9ae95f --- /dev/null +++ b/.git-rewrite/map/a29f6e6968735a388d7964726ad121b3de3d8541 @@ -0,0 +1 @@ +d4caba30ed2e494a7e5b7e20b3a616eccf386d31 diff --git a/.git-rewrite/map/a351dbfffd9de72613b6866b06b0551b7bc66da1 b/.git-rewrite/map/a351dbfffd9de72613b6866b06b0551b7bc66da1 new file mode 100644 index 00000000..458c31c9 --- /dev/null +++ b/.git-rewrite/map/a351dbfffd9de72613b6866b06b0551b7bc66da1 @@ -0,0 +1 @@ +3c6c8a134c52be49e022dc4eb0cc7548bedea974 diff --git a/.git-rewrite/map/aa8822d6f65a962636d703d86a3923eb0e9b8986 b/.git-rewrite/map/aa8822d6f65a962636d703d86a3923eb0e9b8986 new file mode 100644 index 00000000..603c32f9 --- /dev/null +++ b/.git-rewrite/map/aa8822d6f65a962636d703d86a3923eb0e9b8986 @@ -0,0 +1 @@ +c9e6211d5dd7c30818b0eabd8fd582473e052fec diff --git a/.git-rewrite/map/ab5414f7a8d277ac848efce72aba8bbb0381f732 b/.git-rewrite/map/ab5414f7a8d277ac848efce72aba8bbb0381f732 new file mode 100644 index 00000000..1e7c559a --- /dev/null +++ b/.git-rewrite/map/ab5414f7a8d277ac848efce72aba8bbb0381f732 @@ -0,0 +1 @@ +37cf3a493fc8e5ac479b7fd6894bd193faaba39b diff --git a/.git-rewrite/map/ae82fe4a29e95414cb7f8a36ae1a4d8a29ccdb41 b/.git-rewrite/map/ae82fe4a29e95414cb7f8a36ae1a4d8a29ccdb41 new file mode 100644 index 00000000..96c3a760 --- /dev/null +++ b/.git-rewrite/map/ae82fe4a29e95414cb7f8a36ae1a4d8a29ccdb41 @@ -0,0 +1 @@ +a06510b5cc9f2d3a065e9884f3d846b0c4b78db2 diff --git a/.git-rewrite/map/af85abf33225f0c3a665d131be93adfc0b9711ee b/.git-rewrite/map/af85abf33225f0c3a665d131be93adfc0b9711ee new file mode 100644 index 00000000..305dcd27 --- /dev/null +++ b/.git-rewrite/map/af85abf33225f0c3a665d131be93adfc0b9711ee @@ -0,0 +1 @@ +2566daee2b9fe5b575a1931f7b8166bc496ba802 diff --git a/.git-rewrite/map/b65cbcf075e61f09efeea793d972ca0307fc5acb b/.git-rewrite/map/b65cbcf075e61f09efeea793d972ca0307fc5acb new file mode 100644 index 00000000..b29de7cc --- /dev/null +++ b/.git-rewrite/map/b65cbcf075e61f09efeea793d972ca0307fc5acb @@ -0,0 +1 @@ +f38a20eb188d73d1db9fa91407b6a54653100019 diff --git a/.git-rewrite/map/b6ef7b678b5ffc01a58ee475d34ef3417e7f515d b/.git-rewrite/map/b6ef7b678b5ffc01a58ee475d34ef3417e7f515d new file mode 100644 index 00000000..0c5ba920 --- /dev/null +++ b/.git-rewrite/map/b6ef7b678b5ffc01a58ee475d34ef3417e7f515d @@ -0,0 +1 @@ +8611174237d01a3620bf264f366e895a35aa1ce6 diff --git a/.git-rewrite/map/b7a5db4ad0145ec5ccdc6bcc2f471b8898cce7b8 b/.git-rewrite/map/b7a5db4ad0145ec5ccdc6bcc2f471b8898cce7b8 new file mode 100644 index 00000000..89f78520 --- /dev/null +++ b/.git-rewrite/map/b7a5db4ad0145ec5ccdc6bcc2f471b8898cce7b8 @@ -0,0 +1 @@ +69c313ae701c66a8d768c12a61a09cb53be8bb8c diff --git a/.git-rewrite/map/bed86593a447c082403a80cc707c1d394a37fe57 b/.git-rewrite/map/bed86593a447c082403a80cc707c1d394a37fe57 new file mode 100644 index 00000000..ca38d32a --- /dev/null +++ b/.git-rewrite/map/bed86593a447c082403a80cc707c1d394a37fe57 @@ -0,0 +1 @@ +4b79af2babce9ff3c0340ac29df37b441c9f6f28 diff --git a/.git-rewrite/map/c0ec41965b0907a7de2c35eaa1123ea10bd4e73c b/.git-rewrite/map/c0ec41965b0907a7de2c35eaa1123ea10bd4e73c new file mode 100644 index 00000000..56b95099 --- /dev/null +++ b/.git-rewrite/map/c0ec41965b0907a7de2c35eaa1123ea10bd4e73c @@ -0,0 +1 @@ +d9284fed63660b8d1283c4b08bf33c33fc0a647e diff --git a/.git-rewrite/map/c61ca7115ccff4836301d9fa5a1478decc3a36e7 b/.git-rewrite/map/c61ca7115ccff4836301d9fa5a1478decc3a36e7 new file mode 100644 index 00000000..197a558f --- /dev/null +++ b/.git-rewrite/map/c61ca7115ccff4836301d9fa5a1478decc3a36e7 @@ -0,0 +1 @@ +c15f8fb2ba2c16fd02629e21dffcabffdf6a7b3f diff --git a/.git-rewrite/map/c74cf41ddab65477e1ce64823657b405145f1eec b/.git-rewrite/map/c74cf41ddab65477e1ce64823657b405145f1eec new file mode 100644 index 00000000..c9c13cbb --- /dev/null +++ b/.git-rewrite/map/c74cf41ddab65477e1ce64823657b405145f1eec @@ -0,0 +1 @@ +fe0ac66a139ee6dcb9460208ac1b18dd9b737dbd diff --git a/.git-rewrite/map/c9ca599cf0b3ac3ce80fdb222bf46475eeb72ccd b/.git-rewrite/map/c9ca599cf0b3ac3ce80fdb222bf46475eeb72ccd new file mode 100644 index 00000000..5cf89fb7 --- /dev/null +++ b/.git-rewrite/map/c9ca599cf0b3ac3ce80fdb222bf46475eeb72ccd @@ -0,0 +1 @@ +cde317ef9076f27f0932f3a0d427b5ed3e36f949 diff --git a/.git-rewrite/map/cba84bd02a2d8a7e8229f06c6f988097a79ce87a b/.git-rewrite/map/cba84bd02a2d8a7e8229f06c6f988097a79ce87a new file mode 100644 index 00000000..522d95eb --- /dev/null +++ b/.git-rewrite/map/cba84bd02a2d8a7e8229f06c6f988097a79ce87a @@ -0,0 +1 @@ +898a1672c2ef824144cb1440111202adfbc6153f diff --git a/.git-rewrite/map/cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e b/.git-rewrite/map/cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e new file mode 100644 index 00000000..ed7dadb1 --- /dev/null +++ b/.git-rewrite/map/cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e @@ -0,0 +1 @@ +50b64209573ee1177d2cccd42368285c6960d96d diff --git a/.git-rewrite/map/cffb7642618b7d7492892c79baaed661c74b9186 b/.git-rewrite/map/cffb7642618b7d7492892c79baaed661c74b9186 new file mode 100644 index 00000000..89737822 --- /dev/null +++ b/.git-rewrite/map/cffb7642618b7d7492892c79baaed661c74b9186 @@ -0,0 +1 @@ +ca563c3f697777778ba17032f52d124e9494021e diff --git a/.git-rewrite/map/d1cc23f0c1e29a0122af533ef7254e2fc8f67fec b/.git-rewrite/map/d1cc23f0c1e29a0122af533ef7254e2fc8f67fec new file mode 100644 index 00000000..3f13d4bb --- /dev/null +++ b/.git-rewrite/map/d1cc23f0c1e29a0122af533ef7254e2fc8f67fec @@ -0,0 +1 @@ +0fc5017c405cdd6696f00c5a7518135c7118641b diff --git a/.git-rewrite/map/d1d9100a349884f3d900f07b049412cc7859f9d4 b/.git-rewrite/map/d1d9100a349884f3d900f07b049412cc7859f9d4 new file mode 100644 index 00000000..b4fa2653 --- /dev/null +++ b/.git-rewrite/map/d1d9100a349884f3d900f07b049412cc7859f9d4 @@ -0,0 +1 @@ +60b536d81f728686b804a7b94e724ca2b1775bc7 diff --git a/.git-rewrite/map/d37d037abdb869cc8c99879790a038f25e5d0ca3 b/.git-rewrite/map/d37d037abdb869cc8c99879790a038f25e5d0ca3 new file mode 100644 index 00000000..0ff4e6fd --- /dev/null +++ b/.git-rewrite/map/d37d037abdb869cc8c99879790a038f25e5d0ca3 @@ -0,0 +1 @@ +47ab84ad8df03d74929c8c7fb5c98e470e50dee8 diff --git a/.git-rewrite/map/d398ad99c01f80327958f5dfc6100d6ae9672a89 b/.git-rewrite/map/d398ad99c01f80327958f5dfc6100d6ae9672a89 new file mode 100644 index 00000000..4f68b1da --- /dev/null +++ b/.git-rewrite/map/d398ad99c01f80327958f5dfc6100d6ae9672a89 @@ -0,0 +1 @@ +0589131842de182be161d134aed56e29eb619da0 diff --git a/.git-rewrite/map/d505853cab64f497cea0982962e14d4fda6f8e46 b/.git-rewrite/map/d505853cab64f497cea0982962e14d4fda6f8e46 new file mode 100644 index 00000000..3aba0ed4 --- /dev/null +++ b/.git-rewrite/map/d505853cab64f497cea0982962e14d4fda6f8e46 @@ -0,0 +1 @@ +fcb4bfa78adedff29aebb163c9a67a80d653df0d diff --git a/.git-rewrite/map/d7ea9cc771da5b1f3b857a4756008737a49df21f b/.git-rewrite/map/d7ea9cc771da5b1f3b857a4756008737a49df21f new file mode 100644 index 00000000..ca697d66 --- /dev/null +++ b/.git-rewrite/map/d7ea9cc771da5b1f3b857a4756008737a49df21f @@ -0,0 +1 @@ +f417b99e725111978f8cbbc05aae29b9a32c7e8d diff --git a/.git-rewrite/map/d8f7430a030e526ebc88c82881ad6c2926af9627 b/.git-rewrite/map/d8f7430a030e526ebc88c82881ad6c2926af9627 new file mode 100644 index 00000000..f4d8430f --- /dev/null +++ b/.git-rewrite/map/d8f7430a030e526ebc88c82881ad6c2926af9627 @@ -0,0 +1 @@ +c3070d1244bfb6966f7198b472e494ed7f2943b7 diff --git a/.git-rewrite/map/d95d9a6e27a2846d51cb38d94ecbfeca458b4740 b/.git-rewrite/map/d95d9a6e27a2846d51cb38d94ecbfeca458b4740 new file mode 100644 index 00000000..ee21b407 --- /dev/null +++ b/.git-rewrite/map/d95d9a6e27a2846d51cb38d94ecbfeca458b4740 @@ -0,0 +1 @@ +7dc72330963cb7700105a5b44a6aeb41012b3810 diff --git a/.git-rewrite/map/ddc2462be1f5ec22e2a7b1ada1162b96b4fb0a81 b/.git-rewrite/map/ddc2462be1f5ec22e2a7b1ada1162b96b4fb0a81 new file mode 100644 index 00000000..d6fddc64 --- /dev/null +++ b/.git-rewrite/map/ddc2462be1f5ec22e2a7b1ada1162b96b4fb0a81 @@ -0,0 +1 @@ +5d064130d9ca64cd88d45d20816728c4c1a42b2e diff --git a/.git-rewrite/map/deef31d3bcae3b792b171af6791d30b508fa4070 b/.git-rewrite/map/deef31d3bcae3b792b171af6791d30b508fa4070 new file mode 100644 index 00000000..323b18fe --- /dev/null +++ b/.git-rewrite/map/deef31d3bcae3b792b171af6791d30b508fa4070 @@ -0,0 +1 @@ +664c26d59570aa938c055251c4c0a0e75c3cf3be diff --git a/.git-rewrite/map/df52e923c2c127cc37d356a949b66e0ed1f18934 b/.git-rewrite/map/df52e923c2c127cc37d356a949b66e0ed1f18934 new file mode 100644 index 00000000..076f550f --- /dev/null +++ b/.git-rewrite/map/df52e923c2c127cc37d356a949b66e0ed1f18934 @@ -0,0 +1 @@ +94f90b1d0d0dd24711048978dee3d84fd74edf67 diff --git a/.git-rewrite/map/e04e9149451b642a45f1d1dba38e0fa2513ecc9b b/.git-rewrite/map/e04e9149451b642a45f1d1dba38e0fa2513ecc9b new file mode 100644 index 00000000..b29008a5 --- /dev/null +++ b/.git-rewrite/map/e04e9149451b642a45f1d1dba38e0fa2513ecc9b @@ -0,0 +1 @@ +45603837a61b0c424ace993410fece2437bea062 diff --git a/.git-rewrite/map/e0d79e85ffe0616b1ab1f82d235a83363ca4d76c b/.git-rewrite/map/e0d79e85ffe0616b1ab1f82d235a83363ca4d76c new file mode 100644 index 00000000..10958862 --- /dev/null +++ b/.git-rewrite/map/e0d79e85ffe0616b1ab1f82d235a83363ca4d76c @@ -0,0 +1 @@ +a3e53db9ed418fd40869b51847a5dbc365b82c92 diff --git a/.git-rewrite/map/e0f201696fc4000b194485f419c42109440e89c1 b/.git-rewrite/map/e0f201696fc4000b194485f419c42109440e89c1 new file mode 100644 index 00000000..5772d219 --- /dev/null +++ b/.git-rewrite/map/e0f201696fc4000b194485f419c42109440e89c1 @@ -0,0 +1 @@ +80faef8dbdc2ea7028de062ce7a0f75aeec4d9fa diff --git a/.git-rewrite/map/e3bd18d0e0d72258a413972b096cc90f2857e978 b/.git-rewrite/map/e3bd18d0e0d72258a413972b096cc90f2857e978 new file mode 100644 index 00000000..ce004c10 --- /dev/null +++ b/.git-rewrite/map/e3bd18d0e0d72258a413972b096cc90f2857e978 @@ -0,0 +1 @@ +5fb324645d6c7f6616959689f935080772ee23f1 diff --git a/.git-rewrite/map/e5c6cd779ff0164a578159d25dbe5b26947426cc b/.git-rewrite/map/e5c6cd779ff0164a578159d25dbe5b26947426cc new file mode 100644 index 00000000..4d6b26d0 --- /dev/null +++ b/.git-rewrite/map/e5c6cd779ff0164a578159d25dbe5b26947426cc @@ -0,0 +1 @@ +7be393b29e78e80b01dce215e2dd37ff1ce2fc19 diff --git a/.git-rewrite/map/e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 b/.git-rewrite/map/e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 new file mode 100644 index 00000000..fc1f1a2f --- /dev/null +++ b/.git-rewrite/map/e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 @@ -0,0 +1 @@ +b8904afaf334dbfd353133a488839fa6963a2832 diff --git a/.git-rewrite/map/ebc6d4509962f93e933f68de065b83201ac97608 b/.git-rewrite/map/ebc6d4509962f93e933f68de065b83201ac97608 new file mode 100644 index 00000000..507b6be2 --- /dev/null +++ b/.git-rewrite/map/ebc6d4509962f93e933f68de065b83201ac97608 @@ -0,0 +1 @@ +2890522e12e462049d8a6865d344ca310de53526 diff --git a/.git-rewrite/map/eeb965194810c7cac0588aca0157d55c3be3b390 b/.git-rewrite/map/eeb965194810c7cac0588aca0157d55c3be3b390 new file mode 100644 index 00000000..84339eb1 --- /dev/null +++ b/.git-rewrite/map/eeb965194810c7cac0588aca0157d55c3be3b390 @@ -0,0 +1 @@ +e6693ef0c02fc2ae4ffa9d4a00ba65bc9e00c9a8 diff --git a/.git-rewrite/map/eefd7c18df0e4055723b1d765894e68f8d89676c b/.git-rewrite/map/eefd7c18df0e4055723b1d765894e68f8d89676c new file mode 100644 index 00000000..6c18b991 --- /dev/null +++ b/.git-rewrite/map/eefd7c18df0e4055723b1d765894e68f8d89676c @@ -0,0 +1 @@ +1d9cfa3c169758216800b7ae9fa0f6770282949c diff --git a/.git-rewrite/map/efbc84ce2116131a3965891aafd110a7430a77ed b/.git-rewrite/map/efbc84ce2116131a3965891aafd110a7430a77ed new file mode 100644 index 00000000..7a99ea25 --- /dev/null +++ b/.git-rewrite/map/efbc84ce2116131a3965891aafd110a7430a77ed @@ -0,0 +1 @@ +52aa27cfb593339e9d06ab89db2dd3c44f3ca64d diff --git a/.git-rewrite/map/efd7d845ab13a050611f7184cfe9c529fc67e7c2 b/.git-rewrite/map/efd7d845ab13a050611f7184cfe9c529fc67e7c2 new file mode 100644 index 00000000..52c52d40 --- /dev/null +++ b/.git-rewrite/map/efd7d845ab13a050611f7184cfe9c529fc67e7c2 @@ -0,0 +1 @@ +51b8f18fedadb3c58fc6513fcb7bbcd39a230a4d diff --git a/.git-rewrite/map/f00d6c3f254179796a021e1b3f00ee6a8cc66dea b/.git-rewrite/map/f00d6c3f254179796a021e1b3f00ee6a8cc66dea new file mode 100644 index 00000000..cee6aeee --- /dev/null +++ b/.git-rewrite/map/f00d6c3f254179796a021e1b3f00ee6a8cc66dea @@ -0,0 +1 @@ +3471a139a8cc5f091bb5347468a499329a1a8540 diff --git a/.git-rewrite/map/f10126881021f15b7d6070aea776a749f48d46cc b/.git-rewrite/map/f10126881021f15b7d6070aea776a749f48d46cc new file mode 100644 index 00000000..1469deca --- /dev/null +++ b/.git-rewrite/map/f10126881021f15b7d6070aea776a749f48d46cc @@ -0,0 +1 @@ +c6565b1ffe5db86ba8d1c911377fb266f5694f52 diff --git a/.git-rewrite/map/fa9ea30f0ca41717f72ba38ff761a083d7210294 b/.git-rewrite/map/fa9ea30f0ca41717f72ba38ff761a083d7210294 new file mode 100644 index 00000000..6326821c --- /dev/null +++ b/.git-rewrite/map/fa9ea30f0ca41717f72ba38ff761a083d7210294 @@ -0,0 +1 @@ +66400c2d1bedba515e39a79a45cee1c69fa139a1 diff --git a/.git-rewrite/map/fac33b021b5a7e19f53b846732aedb3a4227be98 b/.git-rewrite/map/fac33b021b5a7e19f53b846732aedb3a4227be98 new file mode 100644 index 00000000..eff95a90 --- /dev/null +++ b/.git-rewrite/map/fac33b021b5a7e19f53b846732aedb3a4227be98 @@ -0,0 +1 @@ +bd6f9408329c275e283985d5c73b51baae77a6a8 diff --git a/.git-rewrite/map/fd139a06ea0c890fc3dabbaf5a47cd1284817265 b/.git-rewrite/map/fd139a06ea0c890fc3dabbaf5a47cd1284817265 new file mode 100644 index 00000000..7897f42c --- /dev/null +++ b/.git-rewrite/map/fd139a06ea0c890fc3dabbaf5a47cd1284817265 @@ -0,0 +1 @@ +535281fd896210a4a50e8916182f008473a9f810 diff --git a/.git-rewrite/map/fe70cf41fa55646168106ad00b52c57b39442aa1 b/.git-rewrite/map/fe70cf41fa55646168106ad00b52c57b39442aa1 new file mode 100644 index 00000000..a65f179c --- /dev/null +++ b/.git-rewrite/map/fe70cf41fa55646168106ad00b52c57b39442aa1 @@ -0,0 +1 @@ +84cf73d48c27dabedd0fc05d9c5451c60de92849 diff --git a/.git-rewrite/message b/.git-rewrite/message new file mode 100644 index 00000000..9351b675 --- /dev/null +++ b/.git-rewrite/message @@ -0,0 +1 @@ +apiResponse 응답 수정 제외 완료 diff --git a/.git-rewrite/parse b/.git-rewrite/parse new file mode 100644 index 00000000..e32a6e32 --- /dev/null +++ b/.git-rewrite/parse @@ -0,0 +1,19 @@ +0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 +3ac4011c72d33fa7fc26e7873fb52d91048d2b7e +55f0e49d95aa04487ed94d548ffdd44d0ded8a06 +2968ce57e4274710c986e005c93997ce503eb895 +52fd2147e9bf5905213d955934ff0ab565b4d777 +52fd2147e9bf5905213d955934ff0ab565b4d777 +7acd794e3027d816efe7a4f797ef1c2ad9332a4c +e8750da3afbbf333be3c92b06eb0d45cbabae584 +c724ddb7d6061686a25d8bd381f5555809d2f089 +d28c519f6a0e6d21c11339d8f9ea99e347f5d63f +528c6626c4be064722b39c9a65cea01df5031c55 +55f0e49d95aa04487ed94d548ffdd44d0ded8a06 +dc16f64202b085b8af210eb9d26fbccea3117eac +d49669cc6e54195a81161465953f29137bd116b2 +54c0bd75730eafd65d49141c4aa49f5f58ab38a2 +923cce8cb09c5583bbc7cd242e39f3a9b9afc6dc +4978d556b292ad2f4abc5437ffb096e0120becca +52fd2147e9bf5905213d955934ff0ab565b4d777 +3215d5a7d47df633d1b61bc319df088935f55c00 diff --git a/.git-rewrite/raw-refs b/.git-rewrite/raw-refs new file mode 100644 index 00000000..e7aa58f6 --- /dev/null +++ b/.git-rewrite/raw-refs @@ -0,0 +1,19 @@ +refs/heads/develop +refs/heads/feat/10/user +refs/heads/feat/20/home +refs/heads/feat/26/collectionDetail +refs/heads/main +refs/remotes/origin/main +refs/remotes/origin/bug/11/idea-파일-추적-문제-해결 +refs/remotes/origin/deploy +refs/remotes/origin/develop +refs/remotes/origin/feat/10/user +refs/remotes/origin/feat/14/memo-api-개발 +refs/remotes/origin/feat/20/home +refs/remotes/origin/feat/25/재생-목록-api-개발임베드-기능-구현 +refs/remotes/origin/feat/26/collectionDetail +refs/remotes/origin/feat/26/s3연결 +refs/remotes/origin/feat/29/myPage +refs/remotes/origin/feat/43/email +refs/remotes/origin/main +refs/remotes/origin/revert-36-develop diff --git a/.git-rewrite/revs b/.git-rewrite/revs new file mode 100644 index 00000000..99176294 --- /dev/null +++ b/.git-rewrite/revs @@ -0,0 +1,299 @@ +922edc9106a8d35f712985588ea234178016b267 +3d01e511aec0e6f38b08f6bfe293df28eadc0ef0 922edc9106a8d35f712985588ea234178016b267 +d1d9100a349884f3d900f07b049412cc7859f9d4 3d01e511aec0e6f38b08f6bfe293df28eadc0ef0 +af85abf33225f0c3a665d131be93adfc0b9711ee d1d9100a349884f3d900f07b049412cc7859f9d4 +91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 af85abf33225f0c3a665d131be93adfc0b9711ee +e0d79e85ffe0616b1ab1f82d235a83363ca4d76c 91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 +1256997c50b1a350d832b00706200827711b1566 91ea64fcb97d05cfc3a1fd5c129b7439ab1b2849 +6746d022b3db99e56467434c692d91dfd1f33784 1256997c50b1a350d832b00706200827711b1566 +cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e 6746d022b3db99e56467434c692d91dfd1f33784 +eefd7c18df0e4055723b1d765894e68f8d89676c e0d79e85ffe0616b1ab1f82d235a83363ca4d76c cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e +6d1e3a61c094f511eec2d4240d98e85590e77c67 eefd7c18df0e4055723b1d765894e68f8d89676c +d95d9a6e27a2846d51cb38d94ecbfeca458b4740 6d1e3a61c094f511eec2d4240d98e85590e77c67 +6e2ce634458b0ea3133a5cb9c5c7c19983ca7206 d95d9a6e27a2846d51cb38d94ecbfeca458b4740 +8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce cf85b9b4e04a6a87d2b3f4ec365589a8d9f2f89e 6e2ce634458b0ea3133a5cb9c5c7c19983ca7206 +964df6c74d43a4014cb0b5d79ca1dc69e95f2b78 6d1e3a61c094f511eec2d4240d98e85590e77c67 8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce +2d68a12033ad43f6c109dcd3d704a8449cbefea5 964df6c74d43a4014cb0b5d79ca1dc69e95f2b78 +8851a91a4c942a18b1570f88e070571cf4e8d240 2d68a12033ad43f6c109dcd3d704a8449cbefea5 +b6ef7b678b5ffc01a58ee475d34ef3417e7f515d 8851a91a4c942a18b1570f88e070571cf4e8d240 +1604df6e567b12148948b7b4009eb80b9ccff25e b6ef7b678b5ffc01a58ee475d34ef3417e7f515d +18a914c828173fdfb84fdb9666bfb7485b730941 1604df6e567b12148948b7b4009eb80b9ccff25e +d8f7430a030e526ebc88c82881ad6c2926af9627 18a914c828173fdfb84fdb9666bfb7485b730941 +2bc5b20d61ab922707471dc835c94c5bd4267ad4 d8f7430a030e526ebc88c82881ad6c2926af9627 +2d6e57418118f19fc236e0efe2ae05b9ce747216 2bc5b20d61ab922707471dc835c94c5bd4267ad4 +a048d733cedb1a6780b8f77b134969c1b7f7df7c 2d6e57418118f19fc236e0efe2ae05b9ce747216 +920d0fa6dd901f0d1e74b94b3edbd441a2e090a1 a048d733cedb1a6780b8f77b134969c1b7f7df7c +7fea266ee2de59125833d31cd2e73f15a2521847 920d0fa6dd901f0d1e74b94b3edbd441a2e090a1 +bed86593a447c082403a80cc707c1d394a37fe57 7fea266ee2de59125833d31cd2e73f15a2521847 +cba84bd02a2d8a7e8229f06c6f988097a79ce87a bed86593a447c082403a80cc707c1d394a37fe57 +4a41a86bfa78852f8912c7ef5eb55f1651424000 cba84bd02a2d8a7e8229f06c6f988097a79ce87a +8a587f92e0e299c443f37d07561c541870ac0f61 4a41a86bfa78852f8912c7ef5eb55f1651424000 +2a39a83bd1692c1bdaaa568e6931cc3c5d49b193 8a587f92e0e299c443f37d07561c541870ac0f61 +df52e923c2c127cc37d356a949b66e0ed1f18934 2a39a83bd1692c1bdaaa568e6931cc3c5d49b193 +deef31d3bcae3b792b171af6791d30b508fa4070 df52e923c2c127cc37d356a949b66e0ed1f18934 +8a73db471e5d5ffb03f188c37733cd47a0b1a44d deef31d3bcae3b792b171af6791d30b508fa4070 +7647f7f62d9d14463358e323ef229c68fae8d4f7 8a73db471e5d5ffb03f188c37733cd47a0b1a44d +32267c564f63e143a1bcc06831257fd37909fd99 7647f7f62d9d14463358e323ef229c68fae8d4f7 +2c22217d1127dec9ae39dd84b0570b5bbeea3b99 32267c564f63e143a1bcc06831257fd37909fd99 +a351dbfffd9de72613b6866b06b0551b7bc66da1 2c22217d1127dec9ae39dd84b0570b5bbeea3b99 +c9ca599cf0b3ac3ce80fdb222bf46475eeb72ccd a351dbfffd9de72613b6866b06b0551b7bc66da1 +aa8822d6f65a962636d703d86a3923eb0e9b8986 c9ca599cf0b3ac3ce80fdb222bf46475eeb72ccd +4c0e52ba25fe177c30cbbd970a3c491aae6efd73 aa8822d6f65a962636d703d86a3923eb0e9b8986 +2e0357eb2ac4928ded47a80359f6cb3a4cf11dd8 4c0e52ba25fe177c30cbbd970a3c491aae6efd73 +57cfa1165c473e237f49f157b15c3a50d2b47d30 2e0357eb2ac4928ded47a80359f6cb3a4cf11dd8 +0a4d5316489ceb617515710013c84b154b704e25 57cfa1165c473e237f49f157b15c3a50d2b47d30 +4f1e828bc3607019ae1f13217f147f82ffee0936 0a4d5316489ceb617515710013c84b154b704e25 +3731e945fbabc5389f4841316a4ba0ed5aa8a37c 4f1e828bc3607019ae1f13217f147f82ffee0936 +efd7d845ab13a050611f7184cfe9c529fc67e7c2 3731e945fbabc5389f4841316a4ba0ed5aa8a37c +6ad520067aa82cf9a29af4cc9d9caf755c56f7f4 efd7d845ab13a050611f7184cfe9c529fc67e7c2 +efbc84ce2116131a3965891aafd110a7430a77ed 6ad520067aa82cf9a29af4cc9d9caf755c56f7f4 +b65cbcf075e61f09efeea793d972ca0307fc5acb efbc84ce2116131a3965891aafd110a7430a77ed +819a798a995d665c70cb78fe530357b54872adba b65cbcf075e61f09efeea793d972ca0307fc5acb +b7a5db4ad0145ec5ccdc6bcc2f471b8898cce7b8 819a798a995d665c70cb78fe530357b54872adba +a29f6e6968735a388d7964726ad121b3de3d8541 b7a5db4ad0145ec5ccdc6bcc2f471b8898cce7b8 +fe70cf41fa55646168106ad00b52c57b39442aa1 a29f6e6968735a388d7964726ad121b3de3d8541 +0aed3109ef2251f403ff3f9573522ebb265fd672 fe70cf41fa55646168106ad00b52c57b39442aa1 +1b9e0695764bca055decf36ff0a61507e5f0974f 0aed3109ef2251f403ff3f9573522ebb265fd672 +59b3909e1d5b553ede74b5c5afabc03dac35b784 1b9e0695764bca055decf36ff0a61507e5f0974f +225595e2f112d03031bd65fa7e28a9b82df89750 59b3909e1d5b553ede74b5c5afabc03dac35b784 +e5c6cd779ff0164a578159d25dbe5b26947426cc 225595e2f112d03031bd65fa7e28a9b82df89750 +8bf5e39020a808f2a077cfcc83aebaefb4956608 e5c6cd779ff0164a578159d25dbe5b26947426cc +9f2c9293a69f195a7c26519fbee8aa09fcf70c40 8bf5e39020a808f2a077cfcc83aebaefb4956608 +68f0b732c5bafa9772b9a177c0abb0e926d1f4c6 9f2c9293a69f195a7c26519fbee8aa09fcf70c40 +996e5a7bb14aa3719ceac2b3fa62ce334c5aaba8 68f0b732c5bafa9772b9a177c0abb0e926d1f4c6 +00d077ebd580a3c15cb8839f5031dbc38a7f3959 996e5a7bb14aa3719ceac2b3fa62ce334c5aaba8 +d1cc23f0c1e29a0122af533ef7254e2fc8f67fec 00d077ebd580a3c15cb8839f5031dbc38a7f3959 +8a65b930ce38cce80e4af36572230cf9270e24f0 d1cc23f0c1e29a0122af533ef7254e2fc8f67fec +f10126881021f15b7d6070aea776a749f48d46cc 8a65b930ce38cce80e4af36572230cf9270e24f0 +6550e626ddc9bed759ae80efd9ff4b39f275c5d4 f10126881021f15b7d6070aea776a749f48d46cc +92306d9ae13c2a67fa32a9cb1ae9408845f9d218 6550e626ddc9bed759ae80efd9ff4b39f275c5d4 +fac33b021b5a7e19f53b846732aedb3a4227be98 92306d9ae13c2a67fa32a9cb1ae9408845f9d218 +e0f201696fc4000b194485f419c42109440e89c1 fac33b021b5a7e19f53b846732aedb3a4227be98 +ab5414f7a8d277ac848efce72aba8bbb0381f732 e0f201696fc4000b194485f419c42109440e89c1 +c74cf41ddab65477e1ce64823657b405145f1eec ab5414f7a8d277ac848efce72aba8bbb0381f732 +8bf259f60bf69a59739b518ef688bb28d5c82c0d c74cf41ddab65477e1ce64823657b405145f1eec +34cf04b4b2c695417cea61eabe647102a24cc1f1 8bf259f60bf69a59739b518ef688bb28d5c82c0d +31187961b7a877d99681ecccc92bb11c662489cb 34cf04b4b2c695417cea61eabe647102a24cc1f1 +975fb67b21d0e4091444bd9d5637e4dc3db4ab53 31187961b7a877d99681ecccc92bb11c662489cb +ebc6d4509962f93e933f68de065b83201ac97608 975fb67b21d0e4091444bd9d5637e4dc3db4ab53 +d505853cab64f497cea0982962e14d4fda6f8e46 ebc6d4509962f93e933f68de065b83201ac97608 +d7ea9cc771da5b1f3b857a4756008737a49df21f d505853cab64f497cea0982962e14d4fda6f8e46 +61f7fc386faf3f5fb1f9a5b5aa4a4138bbd8595c d7ea9cc771da5b1f3b857a4756008737a49df21f +1027d3692020a63d227e6013df1672632bd3d26a 61f7fc386faf3f5fb1f9a5b5aa4a4138bbd8595c +6aa7a296f0014a7f728aea69bb464b76a5b50bf1 1027d3692020a63d227e6013df1672632bd3d26a +fd139a06ea0c890fc3dabbaf5a47cd1284817265 6aa7a296f0014a7f728aea69bb464b76a5b50bf1 +28e284d206a8db02b9e23d6ba972a0b0cbdd16ec fd139a06ea0c890fc3dabbaf5a47cd1284817265 +588420029ecebb4fedea59d91a92a158cb2907e9 28e284d206a8db02b9e23d6ba972a0b0cbdd16ec +512da0db748012e1e19995ec91d71ef04de9d43f 588420029ecebb4fedea59d91a92a158cb2907e9 +975af36bcc11682c94324e3b7c85902e3206e793 512da0db748012e1e19995ec91d71ef04de9d43f +d398ad99c01f80327958f5dfc6100d6ae9672a89 975af36bcc11682c94324e3b7c85902e3206e793 +f00d6c3f254179796a021e1b3f00ee6a8cc66dea d398ad99c01f80327958f5dfc6100d6ae9672a89 +ddc2462be1f5ec22e2a7b1ada1162b96b4fb0a81 f00d6c3f254179796a021e1b3f00ee6a8cc66dea +32beed45a4405c3a3e2543140d7c635be272d704 ddc2462be1f5ec22e2a7b1ada1162b96b4fb0a81 +5b9d161239f8f55a1efa96fd8f9df8185b9c7231 32beed45a4405c3a3e2543140d7c635be272d704 +1df55aa5e4a964744f1ff121271371cbd4dfcc33 5b9d161239f8f55a1efa96fd8f9df8185b9c7231 +6be417102c1307c44ff33802a33cd0e11933dbb0 1df55aa5e4a964744f1ff121271371cbd4dfcc33 +e3bd18d0e0d72258a413972b096cc90f2857e978 6be417102c1307c44ff33802a33cd0e11933dbb0 +89fe5fc6e826f2a85add24554798bcb688f6ea49 e3bd18d0e0d72258a413972b096cc90f2857e978 +2f23763e398d690722dd1fd3886d5b33baf8d37a 89fe5fc6e826f2a85add24554798bcb688f6ea49 +747246a0b46d742b5a9087a823a435ec88464d8b 2f23763e398d690722dd1fd3886d5b33baf8d37a +5fdd4033af5d0aa47c072b59a1524fff46e2eeb5 747246a0b46d742b5a9087a823a435ec88464d8b +fa9ea30f0ca41717f72ba38ff761a083d7210294 5fdd4033af5d0aa47c072b59a1524fff46e2eeb5 +50b9a3a2a303c7e8089586ba98ee59898c268ffb fa9ea30f0ca41717f72ba38ff761a083d7210294 +4fb06b940a58d067f13f920bb25b3df8d33f48a1 50b9a3a2a303c7e8089586ba98ee59898c268ffb +115206189608cf37e4cad16622617f47f4b74027 4fb06b940a58d067f13f920bb25b3df8d33f48a1 +9b2f00c1ca8a6e70f24fd44f23fc289a3738f162 115206189608cf37e4cad16622617f47f4b74027 +375274c762f5854e75de7920c5bc426ba4bf86e4 9b2f00c1ca8a6e70f24fd44f23fc289a3738f162 +690adbdfc477591814576166884379457b299afe 375274c762f5854e75de7920c5bc426ba4bf86e4 +c61ca7115ccff4836301d9fa5a1478decc3a36e7 690adbdfc477591814576166884379457b299afe +d37d037abdb869cc8c99879790a038f25e5d0ca3 c61ca7115ccff4836301d9fa5a1478decc3a36e7 +e04e9149451b642a45f1d1dba38e0fa2513ecc9b d37d037abdb869cc8c99879790a038f25e5d0ca3 +2a39d7a9d8da3a355e1a56539c7dc62881e253b2 e04e9149451b642a45f1d1dba38e0fa2513ecc9b +cffb7642618b7d7492892c79baaed661c74b9186 2a39d7a9d8da3a355e1a56539c7dc62881e253b2 +024743bf4a56f00680faf2929f124c114a611be4 cffb7642618b7d7492892c79baaed661c74b9186 +8ce9ceb1c57de4b08667c9221aadb30203abcae9 024743bf4a56f00680faf2929f124c114a611be4 +ae82fe4a29e95414cb7f8a36ae1a4d8a29ccdb41 8ce9ceb1c57de4b08667c9221aadb30203abcae9 +c0ec41965b0907a7de2c35eaa1123ea10bd4e73c ae82fe4a29e95414cb7f8a36ae1a4d8a29ccdb41 +992c59133f0d5ed139567d522f6eb05ffac4fe4a 8f7c240f7ab7b32d2a22f5716fb4bf4d9bd811ce c0ec41965b0907a7de2c35eaa1123ea10bd4e73c +eeb965194810c7cac0588aca0157d55c3be3b390 992c59133f0d5ed139567d522f6eb05ffac4fe4a +79bd2b0ca55bb0de73723bff874c3880d7c46c1a eeb965194810c7cac0588aca0157d55c3be3b390 +7acd794e3027d816efe7a4f797ef1c2ad9332a4c 79bd2b0ca55bb0de73723bff874c3880d7c46c1a +e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 992c59133f0d5ed139567d522f6eb05ffac4fe4a 7acd794e3027d816efe7a4f797ef1c2ad9332a4c +00a198e33ab7d572f8938cca35a166d8558b6566 e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 +fee8d1d2af5869a49af94fb1901ae1267ab51842 00a198e33ab7d572f8938cca35a166d8558b6566 +2a2f41c85e06b6a34c1d3ec83067e1fc416d08e8 fee8d1d2af5869a49af94fb1901ae1267ab51842 +e254c1b011d7b90cf40ec38873f77fe65e3a0324 2a2f41c85e06b6a34c1d3ec83067e1fc416d08e8 +ee365cce6723da466e5d517ea2eaf25c2bc2fc79 992c59133f0d5ed139567d522f6eb05ffac4fe4a +c3c63345fd137dcd4cc06f3d193905b0b502eb52 ee365cce6723da466e5d517ea2eaf25c2bc2fc79 +5d3af3fba1db7b96f0f9138caf49426e87398c93 c3c63345fd137dcd4cc06f3d193905b0b502eb52 +6cad6a3947a17b6c35da560bc13c7362254b4a67 5d3af3fba1db7b96f0f9138caf49426e87398c93 +32a28e62943e04129fa51a61c4a808b6cdb60be1 6cad6a3947a17b6c35da560bc13c7362254b4a67 +ed7b9d18480d61117c581501f7ae5853975d4cc0 32a28e62943e04129fa51a61c4a808b6cdb60be1 +2abbd3967c9acff3f1d30e0a6eec81da65f15de8 ed7b9d18480d61117c581501f7ae5853975d4cc0 +923c315ae468e29796d17f3141716c905016aec2 2abbd3967c9acff3f1d30e0a6eec81da65f15de8 +f854156bd187e23f49ba0c7ff1deb7b80bc3582d 923c315ae468e29796d17f3141716c905016aec2 +dc151cbf1640dc7e51a5da8bc49252becdbb7188 f854156bd187e23f49ba0c7ff1deb7b80bc3582d +96f4cc0926e000c106dbdc73c4bd0ba55e3a091a dc151cbf1640dc7e51a5da8bc49252becdbb7188 +e616fde7c06f887052cd3063357dc18a4324df81 96f4cc0926e000c106dbdc73c4bd0ba55e3a091a +9d614712459e488001fb24c3da82ca34087bdf2f e616fde7c06f887052cd3063357dc18a4324df81 +1393c5a0ca063f203e8d4b6dd223c8b671019ef5 9d614712459e488001fb24c3da82ca34087bdf2f +afe3b5eaa0eb9f006795c826370451a6263f3a1f 1393c5a0ca063f203e8d4b6dd223c8b671019ef5 +cd1f08e1cc909c353bff01353897aaf5451c145f afe3b5eaa0eb9f006795c826370451a6263f3a1f +6b724da5a3c7464f4dc3aa683860376a8b7a3111 cd1f08e1cc909c353bff01353897aaf5451c145f +c01b9b586b9b46f0c58e33982f4b3507352b435d 6b724da5a3c7464f4dc3aa683860376a8b7a3111 +a9939d7e4f84dde581ee66b6c4ed8abeb78de850 c01b9b586b9b46f0c58e33982f4b3507352b435d +b4b63b0c341a734ada5b40842dbdc2bb0c6a093e a9939d7e4f84dde581ee66b6c4ed8abeb78de850 +09c1cf70074d77a8d4753dc9bd3e76349bbcd16d b4b63b0c341a734ada5b40842dbdc2bb0c6a093e +8338abb0fe4d465b9219718770dd31b6fc6e5a5e 09c1cf70074d77a8d4753dc9bd3e76349bbcd16d +f51be8a2b961e5517138fff793e0d3263c7f131a 8338abb0fe4d465b9219718770dd31b6fc6e5a5e +1c17350887c4a7b652e45ce3aac809c35b7a1442 f51be8a2b961e5517138fff793e0d3263c7f131a e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 +8ee20594cc068e3d99a8dc155a43eb89474bafda e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 1c17350887c4a7b652e45ce3aac809c35b7a1442 +8f098b9ae62d7d23fcd7cae8b0365c440de0cd7b 8ee20594cc068e3d99a8dc155a43eb89474bafda +603004e50a73cf17f89a6561fe908fa57f762a41 8f098b9ae62d7d23fcd7cae8b0365c440de0cd7b +d1bdf9373e62d51b257ff3ddb00a84bc1aafa7bc 603004e50a73cf17f89a6561fe908fa57f762a41 +3ac4011c72d33fa7fc26e7873fb52d91048d2b7e d1bdf9373e62d51b257ff3ddb00a84bc1aafa7bc +4704af4dc8010ddc3bb20b0d7ec8788859be76f9 e8025aa8911cfbb95b442f1f2ccc26cc9a04efd8 +e3f735d709f43134620afdfca92259e53773d9bb 8f098b9ae62d7d23fcd7cae8b0365c440de0cd7b 4704af4dc8010ddc3bb20b0d7ec8788859be76f9 +d28c519f6a0e6d21c11339d8f9ea99e347f5d63f 3ac4011c72d33fa7fc26e7873fb52d91048d2b7e e3f735d709f43134620afdfca92259e53773d9bb +97b2121930d0ec3de120bfba651ac625494431a2 e3f735d709f43134620afdfca92259e53773d9bb d28c519f6a0e6d21c11339d8f9ea99e347f5d63f +55ea3773ad434136e35fc4288a3e7b895e3a99d7 97b2121930d0ec3de120bfba651ac625494431a2 +dce9b1bac54ba247ffcc20106831483d705da60a 8f098b9ae62d7d23fcd7cae8b0365c440de0cd7b +8ddba55f3dd559271ddf28195aa36ae3e0db6d11 dce9b1bac54ba247ffcc20106831483d705da60a +80f24745a2d0a9212d153e16d83a17eb253122bb 8ddba55f3dd559271ddf28195aa36ae3e0db6d11 +90820eb4cf11381c539ec931e5ee8115f933e95e 80f24745a2d0a9212d153e16d83a17eb253122bb +4bdc2e104e135c0141d06fe789dd895610936ecb 90820eb4cf11381c539ec931e5ee8115f933e95e +1cc55eedefb0e3c4bee459054d660d10ada0be10 4bdc2e104e135c0141d06fe789dd895610936ecb +aee5f9570f9fe9e720a87eeb781f7e49d4bdbdea 1cc55eedefb0e3c4bee459054d660d10ada0be10 +e74c2ea9a1ff2fa042224d43cbb961f2a065e18a aee5f9570f9fe9e720a87eeb781f7e49d4bdbdea +2ce45b5ca2a582bab833c96d25ee9921cefeb733 e74c2ea9a1ff2fa042224d43cbb961f2a065e18a +1d42e155cf8e79d5efb1f7a40594afc4ba681b2c 2ce45b5ca2a582bab833c96d25ee9921cefeb733 +5ade0e052e95f82c5f9a0ec9881967817beba26f 1d42e155cf8e79d5efb1f7a40594afc4ba681b2c +82ffe87af6ebc339f88e9feee5c92e6d94e9afd0 5ade0e052e95f82c5f9a0ec9881967817beba26f +4c7afa1662d2e97eacd770951962920a85b76065 82ffe87af6ebc339f88e9feee5c92e6d94e9afd0 +d1d30d4f2f6b944e000c7178e4b77150ca5063d4 4c7afa1662d2e97eacd770951962920a85b76065 +59d5008c663a5526828289c5be7d64d80423439b d1d30d4f2f6b944e000c7178e4b77150ca5063d4 +a197505f0c578da76e803fd39500f939e87f603e 59d5008c663a5526828289c5be7d64d80423439b +8a54ec8620ce886168f36c6e8f39fb665e11d27c 55ea3773ad434136e35fc4288a3e7b895e3a99d7 a197505f0c578da76e803fd39500f939e87f603e +528c6626c4be064722b39c9a65cea01df5031c55 e254c1b011d7b90cf40ec38873f77fe65e3a0324 8a54ec8620ce886168f36c6e8f39fb665e11d27c +c0415cf2ec425d8e8b1f1ce601b717266019f596 8a54ec8620ce886168f36c6e8f39fb665e11d27c 528c6626c4be064722b39c9a65cea01df5031c55 +3115fd3e002741d773a42bad29e0a3c1798697e2 8a54ec8620ce886168f36c6e8f39fb665e11d27c +cb644ac130960f936f39644f5b1612af03a1d685 3115fd3e002741d773a42bad29e0a3c1798697e2 +197ee3e118c17afa011d5a279d2fcfa490cc739b cb644ac130960f936f39644f5b1612af03a1d685 +be3b3e835d0e0fe273934ee44c6df9d3997e6573 197ee3e118c17afa011d5a279d2fcfa490cc739b +12104eda3ebe8c9ba76be03743d0cef59bd650bb be3b3e835d0e0fe273934ee44c6df9d3997e6573 +e6ee20be2233e4f84c994c0dc3ca3113bc089817 12104eda3ebe8c9ba76be03743d0cef59bd650bb +304e6015428e0319862573a18321e800ad91d276 e6ee20be2233e4f84c994c0dc3ca3113bc089817 +8e06cc6b364d8fcf285d690c47aa09c3233aa70d 304e6015428e0319862573a18321e800ad91d276 +1d3a14fa0571ae2132ec980ef7e9ad446a0603f1 8e06cc6b364d8fcf285d690c47aa09c3233aa70d +142f8a03c2825ca2bb0cf23cdd87cc05746839e4 1d3a14fa0571ae2132ec980ef7e9ad446a0603f1 +39033136867cbc71ffb53d59bec59a0b27c91b97 142f8a03c2825ca2bb0cf23cdd87cc05746839e4 +99c83a7045008243662130384488b6deb119b1d5 39033136867cbc71ffb53d59bec59a0b27c91b97 +72b10761fbff8d1fe5b3f91e2c9205cda67e8abf 99c83a7045008243662130384488b6deb119b1d5 +b75d070ab8a85150d00983cc56d1c7b52cc9d7f1 72b10761fbff8d1fe5b3f91e2c9205cda67e8abf +d6c22db4b758bd78cd835f80e04177aee66bc9d5 b75d070ab8a85150d00983cc56d1c7b52cc9d7f1 +b62a48d9a24dd85d0a85483c23e1c2f5984eba78 d6c22db4b758bd78cd835f80e04177aee66bc9d5 +67bf714e6cdedc0b7611d4d950be667a6e113139 b62a48d9a24dd85d0a85483c23e1c2f5984eba78 +b8773242a32aad76f786cc0c9c96ebb48742a52e 67bf714e6cdedc0b7611d4d950be667a6e113139 +436c21204d45ff66e9a4e204c70a39d4dde4c040 c0415cf2ec425d8e8b1f1ce601b717266019f596 b8773242a32aad76f786cc0c9c96ebb48742a52e +7db62662ee87eb1cb1829cfffa25f50b3eed0884 436c21204d45ff66e9a4e204c70a39d4dde4c040 +5049face916c62bd2d19352328715cb551bceaa4 7db62662ee87eb1cb1829cfffa25f50b3eed0884 +6a96b2ed6ed14a70935116810a15512556467765 5049face916c62bd2d19352328715cb551bceaa4 +b027f1ef606c7ace206a152dda036323d54e148e 6a96b2ed6ed14a70935116810a15512556467765 +092f9eefe1d6554f8a45a38fb45f5209f66012a9 b027f1ef606c7ace206a152dda036323d54e148e +e75b772ac954bc791df8722399cb41c2853468c5 092f9eefe1d6554f8a45a38fb45f5209f66012a9 +5b2c0b997ae50bfa03ad5d8f36c4716970d78a7a e75b772ac954bc791df8722399cb41c2853468c5 +eadc852cd6f12812f654a4086923f2406b5ef6cd 5b2c0b997ae50bfa03ad5d8f36c4716970d78a7a +dc16f64202b085b8af210eb9d26fbccea3117eac eadc852cd6f12812f654a4086923f2406b5ef6cd +c22291af15f9bd96219b2d5fe72779944569ef03 7acd794e3027d816efe7a4f797ef1c2ad9332a4c +bdcd258d5d574b87f184307eff798cc5938567bb c22291af15f9bd96219b2d5fe72779944569ef03 +56225116ba983594fa7b40484068fa610ef01098 bdcd258d5d574b87f184307eff798cc5938567bb +e4610710e032c75e89b74f7992261867b6f90305 56225116ba983594fa7b40484068fa610ef01098 +45d5114ac7d5d884f9688f68c929a1e99d0c5f5b 7acd794e3027d816efe7a4f797ef1c2ad9332a4c +6450185cf3c5522e9cb4825d2c03b978bc1ac3f1 45d5114ac7d5d884f9688f68c929a1e99d0c5f5b +d4f2b871fa1fffd48596b183a93f20d5f081f070 6450185cf3c5522e9cb4825d2c03b978bc1ac3f1 +27fdfa5b3cf6efece081029a8ef294922f948171 436c21204d45ff66e9a4e204c70a39d4dde4c040 +4bb59c56ec190caf7482cc904753399106bb51f4 d4f2b871fa1fffd48596b183a93f20d5f081f070 27fdfa5b3cf6efece081029a8ef294922f948171 +e77f6b830db746f9e6c6835f0b26338110d543f8 4bb59c56ec190caf7482cc904753399106bb51f4 +2fb425bf0a9a8c92d9fb44c3b41d78b9b8f194ec 436c21204d45ff66e9a4e204c70a39d4dde4c040 +eec9c7b84e85b5750d257f712ad054d7f0617bb4 2fb425bf0a9a8c92d9fb44c3b41d78b9b8f194ec +809772569e75807846e85173fdc84e0dd682a040 eec9c7b84e85b5750d257f712ad054d7f0617bb4 +49d400630ff4f123aa66dbf7509aa9b51be01813 809772569e75807846e85173fdc84e0dd682a040 +146b2f37c98f5dc41545c39eed8a0ba1f30adb6b 49d400630ff4f123aa66dbf7509aa9b51be01813 +acce2aa88a741de524cc084c5693045c7ae06e3a 146b2f37c98f5dc41545c39eed8a0ba1f30adb6b +86c5b31282f85c0c7e2e618e9724d9f91e9786d3 acce2aa88a741de524cc084c5693045c7ae06e3a +f764703811c47b6ba4ffabe48242e04ca8b62797 86c5b31282f85c0c7e2e618e9724d9f91e9786d3 +08f17afe1db884a71afc8baa59bb4fe6c1a42fff f764703811c47b6ba4ffabe48242e04ca8b62797 +2fd1af76f7bc1cd1877c87f97461a694c6f0eb13 08f17afe1db884a71afc8baa59bb4fe6c1a42fff +3a5862ad08484f34cfd0070bdddda3afd130ac13 2fd1af76f7bc1cd1877c87f97461a694c6f0eb13 +b582db5d083e49fa0f312c3379f888a1ceb8e7b1 3a5862ad08484f34cfd0070bdddda3afd130ac13 +ee0b3ff0ce84c2480047f821bce097c70857343c b582db5d083e49fa0f312c3379f888a1ceb8e7b1 +2ecfb5d8fde40ad966fb44f726ed56a8149fe782 ee0b3ff0ce84c2480047f821bce097c70857343c +d197be9d80f8352609d45bb72f293371eae30a75 2ecfb5d8fde40ad966fb44f726ed56a8149fe782 +0faa32a841cb4cda5e89785b5c370478df19b1d3 d197be9d80f8352609d45bb72f293371eae30a75 +78dfe5825204700449e16729a0cf2407cd2f1000 0faa32a841cb4cda5e89785b5c370478df19b1d3 +41aa2af6a5cefdc0fe9368e0eaec8592a0e7ebec 78dfe5825204700449e16729a0cf2407cd2f1000 +a686bd80cc6b342fa65cbaf9121a9a860a4f888b 27fdfa5b3cf6efece081029a8ef294922f948171 41aa2af6a5cefdc0fe9368e0eaec8592a0e7ebec +11a4ce3c83dd87fd11701baea020868f9bed602d e77f6b830db746f9e6c6835f0b26338110d543f8 a686bd80cc6b342fa65cbaf9121a9a860a4f888b +a6c9a68666eabb4e53bca282d8e8c711425c79cf e4610710e032c75e89b74f7992261867b6f90305 11a4ce3c83dd87fd11701baea020868f9bed602d +d342be8efc5913376e114f28eb4db2f77fd0d367 11a4ce3c83dd87fd11701baea020868f9bed602d a6c9a68666eabb4e53bca282d8e8c711425c79cf +2bd63fb5fe223f722a628c3a7b890611b77a4466 d342be8efc5913376e114f28eb4db2f77fd0d367 +510a9bb588e653db42088800cdd31bc2e959a69d d342be8efc5913376e114f28eb4db2f77fd0d367 +8079e7c87bf404448fdba17c6030ff4c9a81239b 2bd63fb5fe223f722a628c3a7b890611b77a4466 510a9bb588e653db42088800cdd31bc2e959a69d +d72692ab5ba314c55286a5e07cb49e96656bd89d 8079e7c87bf404448fdba17c6030ff4c9a81239b +13b755ca27e84499ef7033f2ff722b18db8d7536 d72692ab5ba314c55286a5e07cb49e96656bd89d +608355ca7b1db6127f3560a1e9429aac3e8c3d43 13b755ca27e84499ef7033f2ff722b18db8d7536 +8f121dd21344cd581f7ce34123d1e9a992206c4a 608355ca7b1db6127f3560a1e9429aac3e8c3d43 +8e95a7c50cf6470c619692a862494d33af2809f5 8f121dd21344cd581f7ce34123d1e9a992206c4a +52fd2147e9bf5905213d955934ff0ab565b4d777 a6c9a68666eabb4e53bca282d8e8c711425c79cf 8e95a7c50cf6470c619692a862494d33af2809f5 +aac15731ad516e05a3970d2dd81d3b11a3395402 436c21204d45ff66e9a4e204c70a39d4dde4c040 +97dace4f7486015421eb14a032caa7aac4afcf13 aac15731ad516e05a3970d2dd81d3b11a3395402 a686bd80cc6b342fa65cbaf9121a9a860a4f888b +55e2a7c57451a464268d0666fb19adf571b6fd96 97dace4f7486015421eb14a032caa7aac4afcf13 +1f84361106728edfc0985147a684f3f79c169023 55e2a7c57451a464268d0666fb19adf571b6fd96 +758feec396ceecbdd0aedbae91e0c46dc1f75685 a686bd80cc6b342fa65cbaf9121a9a860a4f888b +78d3381b04d834a0b0f730f4364376b18965879b 758feec396ceecbdd0aedbae91e0c46dc1f75685 +c3496f49fb79cddce3133019fbf1d3adbe3f4d72 78d3381b04d834a0b0f730f4364376b18965879b +748a39e274040a62a057b93bf42a254169628976 c3496f49fb79cddce3133019fbf1d3adbe3f4d72 +2cfcb4c416c1e3667e2dd4cb322d7f2f2311a34b 748a39e274040a62a057b93bf42a254169628976 +80a190421a3043300ca2d9f82afd09aa579a6894 2cfcb4c416c1e3667e2dd4cb322d7f2f2311a34b +9f096e6c26c16b4fdd487b85fd30a7b194e7840d 80a190421a3043300ca2d9f82afd09aa579a6894 +fba60830ff8d935248fd7015963dfd014652fde2 9f096e6c26c16b4fdd487b85fd30a7b194e7840d +cc365002b3e757f9e49723103387301b9b6e19b6 fba60830ff8d935248fd7015963dfd014652fde2 +31e3c7ef94c5a70396cfe77cb1fca7992d84b374 cc365002b3e757f9e49723103387301b9b6e19b6 +923cce8cb09c5583bbc7cd242e39f3a9b9afc6dc 31e3c7ef94c5a70396cfe77cb1fca7992d84b374 +32322875ca641000d16e3de688f7cf4ca2c6a5f6 a686bd80cc6b342fa65cbaf9121a9a860a4f888b 923cce8cb09c5583bbc7cd242e39f3a9b9afc6dc +2ae8ca088dee41fff7a66991b28d4b75d653e2bf 1f84361106728edfc0985147a684f3f79c169023 32322875ca641000d16e3de688f7cf4ca2c6a5f6 +a72fb568c5e16b446d07bb3cc9be418a7f3b079d 2ae8ca088dee41fff7a66991b28d4b75d653e2bf +896c38f007b27b3e65ea8fc43850ab3ff53b8c32 a72fb568c5e16b446d07bb3cc9be418a7f3b079d +4ab495f4c4ba4d36bacc5bc4313472a2402711e6 896c38f007b27b3e65ea8fc43850ab3ff53b8c32 +2e73f56552cc220478d45355fcdd68150088129e 4ab495f4c4ba4d36bacc5bc4313472a2402711e6 +0b9ee04582ed66b628703f48674ddb27e1ad4b6d 2e73f56552cc220478d45355fcdd68150088129e +6fdcf12a35b3377a6848a139e1fb344ff9c9db45 0b9ee04582ed66b628703f48674ddb27e1ad4b6d +85bd793785df26f68a66b3db3640394154be8ad6 6fdcf12a35b3377a6848a139e1fb344ff9c9db45 +8a38a2ee215c5ee65b01128cff833424b2d3f0aa 85bd793785df26f68a66b3db3640394154be8ad6 +5773acbb92fc3b4a08d88b074675a5ad7b80ee0c 8a38a2ee215c5ee65b01128cff833424b2d3f0aa +8b96c71a3047cf73ad796000b34192d8851ef904 5773acbb92fc3b4a08d88b074675a5ad7b80ee0c +b16a4cb782655cdcd028fc6d388a95b5b6f17f9e 8b96c71a3047cf73ad796000b34192d8851ef904 +508f05fb136f0876d07ad277bdb0b87b13923c37 b16a4cb782655cdcd028fc6d388a95b5b6f17f9e +c775f698ab0fc68521d2d9ae6bb4cf23b563b5a6 508f05fb136f0876d07ad277bdb0b87b13923c37 +b7a70170635ca8086a0c9f7d203df52dc8369b0e c775f698ab0fc68521d2d9ae6bb4cf23b563b5a6 +beddc18fd8bcdbcf116bbbd131b99f29013e1fc0 b7a70170635ca8086a0c9f7d203df52dc8369b0e +55f0e49d95aa04487ed94d548ffdd44d0ded8a06 beddc18fd8bcdbcf116bbbd131b99f29013e1fc0 +510e6a8dcd3e93f3e393ab25dd84cb380e7bca5c 32322875ca641000d16e3de688f7cf4ca2c6a5f6 8e95a7c50cf6470c619692a862494d33af2809f5 +9c0eb9061f69c563d96b09be2c960ba09b7d34a8 55f0e49d95aa04487ed94d548ffdd44d0ded8a06 510e6a8dcd3e93f3e393ab25dd84cb380e7bca5c +89f8aa4f1efd901e07455c45096ae74f22fe8571 510e6a8dcd3e93f3e393ab25dd84cb380e7bca5c 9c0eb9061f69c563d96b09be2c960ba09b7d34a8 +c9210aed86b5525e2be1e05796df28e31005c91b 510e6a8dcd3e93f3e393ab25dd84cb380e7bca5c 89f8aa4f1efd901e07455c45096ae74f22fe8571 +3215d5a7d47df633d1b61bc319df088935f55c00 c9210aed86b5525e2be1e05796df28e31005c91b +c1d55534111b730c434e00b34c8d8ab296447f8c c9210aed86b5525e2be1e05796df28e31005c91b 3215d5a7d47df633d1b61bc319df088935f55c00 +fdb373e4a61c9d9d70f4680339821610eb60959b c1d55534111b730c434e00b34c8d8ab296447f8c +d514dc5e9e44217e5d3cbe773ff297b57042e441 fdb373e4a61c9d9d70f4680339821610eb60959b +54c0bd75730eafd65d49141c4aa49f5f58ab38a2 d514dc5e9e44217e5d3cbe773ff297b57042e441 +63460421f68c00dafb0065f73e4468fb257a0a7d 89f8aa4f1efd901e07455c45096ae74f22fe8571 +5530ce2642d449c0454f617b95a07b8186809940 63460421f68c00dafb0065f73e4468fb257a0a7d +079979a7d1239dfb912e5351b6c03e3480265ea5 5530ce2642d449c0454f617b95a07b8186809940 +0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 89f8aa4f1efd901e07455c45096ae74f22fe8571 079979a7d1239dfb912e5351b6c03e3480265ea5 +c95f515d675ba4a5374e12aefdc8ef14fc419680 d514dc5e9e44217e5d3cbe773ff297b57042e441 0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 +4978d556b292ad2f4abc5437ffb096e0120becca 0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 +c724ddb7d6061686a25d8bd381f5555809d2f089 0a1dd05039a2aecc76cb746b8bfa6bf9566f6f67 4978d556b292ad2f4abc5437ffb096e0120becca +e8750da3afbbf333be3c92b06eb0d45cbabae584 c95f515d675ba4a5374e12aefdc8ef14fc419680 c724ddb7d6061686a25d8bd381f5555809d2f089 +d49669cc6e54195a81161465953f29137bd116b2 41aa2af6a5cefdc0fe9368e0eaec8592a0e7ebec c724ddb7d6061686a25d8bd381f5555809d2f089 +2968ce57e4274710c986e005c93997ce503eb895 d49669cc6e54195a81161465953f29137bd116b2 diff --git a/learningFlow/.gitattributes b/.gitattributes similarity index 100% rename from learningFlow/.gitattributes rename to .gitattributes diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..3653a42a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,64 @@ +name: CI/CD for Spring Boot App + +on: + push: + branches: + - deploy + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v3 + + # Log in to Docker Hub + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Set up Docker Buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Build and push Docker image + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . # Dockerfile이 있는 디렉토리 + file: ./Dockerfile # Dockerfile 경로 지정 + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }} + + deploy: + runs-on: ubuntu-latest + needs: [docker] + + steps: + - name: Deploy to Server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + envs: GITHUB_SHA + script: | + sudo docker ps -qa | xargs -r sudo docker rm -f + sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}:latest + sudo docker run -d -p 8080:8080 \ + -e MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }} \ + -e MAIL_USERNAME=${{ secrets.MAIL_USERNAME }} \ + -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID }} \ + -e SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }} \ + -e JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \ + -e DB_URL=${{ secrets.DB_URL }} \ + -e DB_USER=${{ secrets.DB_USER }} \ + -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \ + -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ + -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ + ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPONAME }}:latest + sudo docker image prune -f diff --git a/.gitignore b/.gitignore index 5e6c32fc..f6412ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ HELP.md - .gradle -learningFlow/learningFlow-BE/.gradle build/ !gradle/wrapper/gradle-wrapper.jar @@ -22,7 +20,7 @@ bin/ ### IntelliJ IDEA ### .idea -#../.idea +../.idea *.iws *.iml @@ -41,4 +39,10 @@ out/ ### VS Code ### .vscode/ .idea/ -.DS_Store \ No newline at end of file +.DS_Store + +### gradle ### +.gradle/ + +### 환경변수 ### +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..baafd29b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM gradle:8.11.1-jdk21 AS builder +#Gradle 8.11.1 +COPY . /usr/src +WORKDIR /usr/src +RUN gradle wrapper --gradle-version 8.11.1 +RUN ./gradlew clean build -x test + +FROM openjdk:21-jdk +#debian기반 +COPY --from=builder /usr/src/build/libs/learningFlow-BE-0.0.1-SNAPSHOT.jar /usr/app/app.jar +ENTRYPOINT ["java", "-jar", "/usr/app/app.jar"] \ No newline at end of file diff --git a/learningFlow/build.gradle b/build.gradle similarity index 70% rename from learningFlow/build.gradle rename to build.gradle index e061575a..752cb66c 100644 --- a/learningFlow/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ configurations { repositories { mavenCentral() + maven { url 'https://aws.oss.sonatype.org/content/repositories/releases/' } // AWS Repository 추가 } dependencies { @@ -30,8 +31,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // Selenium WebDriver + implementation 'org.seleniumhq.selenium:selenium-java:4.28.1' + + // WebDriver Manager (ChromeDriver 자동 다운로드) +// implementation 'io.github.bonigarcia:webdrivermanager:5.9.2' + // 데이터베이스 - implementation 'mysql:mysql-connector-java:8.0.33' +// implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'com.mysql:mysql-connector-j' + // AWS RDS 및 JDBC 드라이버 의존성 추가 + runtimeOnly 'software.aws.rds:aws-mysql-jdbc:1.1.6' + runtimeOnly 'software.amazon.awssdk:rds:2.20.57' // 보안 implementation 'org.springframework.boot:spring-boot-starter-security' @@ -44,9 +55,6 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - // Redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - // 템플릿 엔진 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' @@ -65,11 +73,17 @@ dependencies { //QueryDsl - implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + implementation 'com.querydsl:querydsl-core' annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + //AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // AWS SDK + implementation 'software.amazon.awssdk:lambda:2.30.12' + } tasks.named('test') { @@ -91,6 +105,11 @@ tasks.withType(JavaCompile).configureEach { options.generatedSourceOutputDirectory = file(generated) } +// gradle clean 시에 QClass 디렉토리 삭제 +clean { + delete file(generated) +} + configurations { compileOnly { extendsFrom annotationProcessor diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2de9c1cb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' # docker-compose 파일 버전 + +services: + learningFlow: # Spring Boot 서비스 이름 + build: + context: . # Dockerfile이 있는 디렉토리 (현재 디렉토리) + image: app-image # 생성될 Docker 이미지 이름 + ports: + - "8080:8080" # 애플리케이션 포트 매핑 + # image: mysql:8.0 # 사용할 Docker 이미지 (MySQL 8.0) + container_name: learningFlow_db # 컨테이너 이름 + restart: always # 컨테이너 재시작 정책 + environment: + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_USERNAME: ${MAIL_USERNAME} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + DB_URL: ${DB_URL} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + +volumes: + db_data: + +networks: + default: + driver: bridge \ No newline at end of file diff --git a/learningFlow/erd.md b/erd.md similarity index 100% rename from learningFlow/erd.md rename to erd.md diff --git a/learningFlow/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from learningFlow/gradle/wrapper/gradle-wrapper.jar rename to gradle/wrapper/gradle-wrapper.jar diff --git a/learningFlow/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from learningFlow/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties diff --git a/learningFlow/gradlew b/gradlew similarity index 100% rename from learningFlow/gradlew rename to gradlew diff --git a/learningFlow/gradlew.bat b/gradlew.bat similarity index 100% rename from learningFlow/gradlew.bat rename to gradlew.bat diff --git a/learningFlow/.DS_Store b/learningFlow/.DS_Store deleted file mode 100644 index 6ae37d3a..00000000 Binary files a/learningFlow/.DS_Store and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/checksums/checksums.lock b/learningFlow/.gradle/8.11.1/checksums/checksums.lock deleted file mode 100644 index 1f74cc16..00000000 Binary files a/learningFlow/.gradle/8.11.1/checksums/checksums.lock and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/checksums/md5-checksums.bin b/learningFlow/.gradle/8.11.1/checksums/md5-checksums.bin deleted file mode 100644 index 9de44878..00000000 Binary files a/learningFlow/.gradle/8.11.1/checksums/md5-checksums.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/checksums/sha1-checksums.bin b/learningFlow/.gradle/8.11.1/checksums/sha1-checksums.bin deleted file mode 100644 index 262370b2..00000000 Binary files a/learningFlow/.gradle/8.11.1/checksums/sha1-checksums.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.bin b/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.bin deleted file mode 100644 index b35adaac..00000000 Binary files a/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.lock b/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.lock deleted file mode 100644 index bff1cbd6..00000000 Binary files a/learningFlow/.gradle/8.11.1/executionHistory/executionHistory.lock and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/fileChanges/last-build.bin b/learningFlow/.gradle/8.11.1/fileChanges/last-build.bin deleted file mode 100644 index f76dd238..00000000 Binary files a/learningFlow/.gradle/8.11.1/fileChanges/last-build.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.bin b/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.bin deleted file mode 100644 index 37c081bb..00000000 Binary files a/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.lock b/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.lock deleted file mode 100644 index 82d6d142..00000000 Binary files a/learningFlow/.gradle/8.11.1/fileHashes/fileHashes.lock and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/fileHashes/resourceHashesCache.bin b/learningFlow/.gradle/8.11.1/fileHashes/resourceHashesCache.bin deleted file mode 100644 index 8858027c..00000000 Binary files a/learningFlow/.gradle/8.11.1/fileHashes/resourceHashesCache.bin and /dev/null differ diff --git a/learningFlow/.gradle/8.11.1/gc.properties b/learningFlow/.gradle/8.11.1/gc.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/learningFlow/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/learningFlow/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 02900265..00000000 Binary files a/learningFlow/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/learningFlow/.gradle/buildOutputCleanup/cache.properties b/learningFlow/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index a5e72f1b..00000000 --- a/learningFlow/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Thu Jan 09 13:28:54 KST 2025 -gradle.version=8.11.1 diff --git a/learningFlow/.gradle/buildOutputCleanup/outputFiles.bin b/learningFlow/.gradle/buildOutputCleanup/outputFiles.bin deleted file mode 100644 index 8be0f275..00000000 Binary files a/learningFlow/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ diff --git a/learningFlow/.gradle/file-system.probe b/learningFlow/.gradle/file-system.probe deleted file mode 100644 index d8a54883..00000000 Binary files a/learningFlow/.gradle/file-system.probe and /dev/null differ diff --git a/learningFlow/.gradle/vcs-1/gc.properties b/learningFlow/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/learningFlow/docker-compose.yml b/learningFlow/docker-compose.yml deleted file mode 100644 index 60850565..00000000 --- a/learningFlow/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3.8' # docker-compose 파일 버전 - -services: - learningFlow: # 서비스 이름 - image: mysql:8.0 # 사용할 Docker 이미지 (MySQL 8.0) - container_name: learningFlow_db # 컨테이너 이름 - restart: always # 컨테이너 재시작 정책 - environment: - MYSQL_ROOT_PASSWORD: root1234 # 루트 사용자 비밀번호 - MYSQL_DATABASE: learningFlow_db # 기본 생성 DB 이름 - MYSQL_USER: learningFlow # 사용자 이름 - MYSQL_PASSWORD: learningFlow123 # 사용자 비밀번호 - ports: - - "3307:3306" # 호스트와 컨테이너 간 포트 매핑 - volumes: - - db_data:/var/lib/mysql # 데이터 지속성을 위한 볼륨 매핑 - -volumes: - db_data: diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QBaseEntity.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QBaseEntity.java deleted file mode 100644 index dd141e5b..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QBaseEntity.java +++ /dev/null @@ -1,39 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBaseEntity is a Querydsl query type for BaseEntity - */ -@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") -public class QBaseEntity extends EntityPathBase { - - private static final long serialVersionUID = -301952604L; - - public static final QBaseEntity baseEntity = new QBaseEntity("baseEntity"); - - public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); - - public final DateTimePath updatedAt = createDateTime("updatedAt", java.time.LocalDateTime.class); - - public QBaseEntity(String variable) { - super(BaseEntity.class, forVariable(variable)); - } - - public QBaseEntity(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBaseEntity(PathMetadata metadata) { - super(BaseEntity.class, metadata); - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollection.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollection.java deleted file mode 100644 index f0fa9030..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollection.java +++ /dev/null @@ -1,79 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QCollection is a Querydsl query type for Collection - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QCollection extends EntityPathBase { - - private static final long serialVersionUID = -819230162L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QCollection collection = new QCollection("collection"); - - public final QBaseEntity _super = new QBaseEntity(this); - - public final NumberPath amount = createNumber("amount", Integer.class); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath creator = createString("creator"); - - public final StringPath detailInformation = createString("detailInformation"); - - public final ListPath> difficulty = this.>createList("difficulty", Integer.class, NumberPath.class, PathInits.DIRECT2); - - public final ListPath episodes = this.createList("episodes", CollectionEpisode.class, QCollectionEpisode.class, PathInits.DIRECT2); - - public final NumberPath id = createNumber("id", Long.class); - - public final QImage image; - - public final EnumPath interestField = createEnum("interestField", learningFlow.learningFlow_BE.domain.enums.InterestField.class); - - public final ListPath keywords = this.createList("keywords", String.class, StringPath.class, PathInits.DIRECT2); - - public final EnumPath mediaType = createEnum("mediaType", learningFlow.learningFlow_BE.domain.enums.MediaType.class); - - public final StringPath title = createString("title"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final ListPath userCollections = this.createList("userCollections", UserCollection.class, QUserCollection.class, PathInits.DIRECT2); - - public QCollection(String variable) { - this(Collection.class, forVariable(variable), INITS); - } - - public QCollection(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QCollection(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QCollection(PathMetadata metadata, PathInits inits) { - this(Collection.class, metadata, inits); - } - - public QCollection(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.image = inits.isInitialized("image") ? new QImage(forProperty("image")) : null; - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollectionEpisode.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollectionEpisode.java deleted file mode 100644 index a0cd685b..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QCollectionEpisode.java +++ /dev/null @@ -1,58 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QCollectionEpisode is a Querydsl query type for CollectionEpisode - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QCollectionEpisode extends EntityPathBase { - - private static final long serialVersionUID = -297427987L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QCollectionEpisode collectionEpisode = new QCollectionEpisode("collectionEpisode"); - - public final QCollection collection; - - public final StringPath episodeName = createString("episodeName"); - - public final NumberPath episodeNumber = createNumber("episodeNumber", Integer.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final QResource resource; - - public QCollectionEpisode(String variable) { - this(CollectionEpisode.class, forVariable(variable), INITS); - } - - public QCollectionEpisode(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QCollectionEpisode(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QCollectionEpisode(PathMetadata metadata, PathInits inits) { - this(CollectionEpisode.class, metadata, inits); - } - - public QCollectionEpisode(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.collection = inits.isInitialized("collection") ? new QCollection(forProperty("collection"), inits.get("collection")) : null; - this.resource = inits.isInitialized("resource") ? new QResource(forProperty("resource")) : null; - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java deleted file mode 100644 index 39ab4679..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java +++ /dev/null @@ -1,53 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QEmailVerificationToken is a Querydsl query type for EmailVerificationToken - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QEmailVerificationToken extends EntityPathBase { - - private static final long serialVersionUID = -1162432526L; - - public static final QEmailVerificationToken emailVerificationToken = new QEmailVerificationToken("emailVerificationToken"); - - public final QBaseEntity _super = new QBaseEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath email = createString("email"); - - public final DateTimePath expiryDate = createDateTime("expiryDate", java.time.LocalDateTime.class); - - public final StringPath password = createString("password"); - - public final StringPath token = createString("token"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final BooleanPath verified = createBoolean("verified"); - - public QEmailVerificationToken(String variable) { - super(EmailVerificationToken.class, forVariable(variable)); - } - - public QEmailVerificationToken(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QEmailVerificationToken(PathMetadata metadata) { - super(EmailVerificationToken.class, metadata); - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QImage.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QImage.java deleted file mode 100644 index 83f4cd1e..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QImage.java +++ /dev/null @@ -1,48 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QImage is a Querydsl query type for Image - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QImage extends EntityPathBase { - - private static final long serialVersionUID = -41036853L; - - public static final QImage image = new QImage("image"); - - public final ListPath collections = this.createList("collections", Collection.class, QCollection.class, PathInits.DIRECT2); - - public final NumberPath fileSize = createNumber("fileSize", java.math.BigInteger.class); - - public final StringPath fileType = createString("fileType"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath imageURL = createString("imageURL"); - - public final ListPath users = this.createList("users", User.class, QUser.class, PathInits.DIRECT2); - - public QImage(String variable) { - super(Image.class, forVariable(variable)); - } - - public QImage(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QImage(PathMetadata metadata) { - super(Image.class, metadata); - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemo.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemo.java deleted file mode 100644 index 228ad549..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemo.java +++ /dev/null @@ -1,59 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QMemo is a Querydsl query type for Memo - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QMemo extends EntityPathBase { - - private static final long serialVersionUID = -1386685238L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QMemo memo = new QMemo("memo"); - - public final QBaseEntity _super = new QBaseEntity(this); - - public final StringPath contents = createString("contents"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QMemoId id; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QMemo(String variable) { - this(Memo.class, forVariable(variable), INITS); - } - - public QMemo(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QMemo(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QMemo(PathMetadata metadata, PathInits inits) { - this(Memo.class, metadata, inits); - } - - public QMemo(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.id = inits.isInitialized("id") ? new QMemoId(forProperty("id")) : null; - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemoId.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemoId.java deleted file mode 100644 index 9874dad6..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QMemoId.java +++ /dev/null @@ -1,39 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QMemoId is a Querydsl query type for MemoId - */ -@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") -public class QMemoId extends BeanPath { - - private static final long serialVersionUID = -1164649595L; - - public static final QMemoId memoId = new QMemoId("memoId"); - - public final NumberPath collectionEpisodeId = createNumber("collectionEpisodeId", Long.class); - - public final StringPath userId = createString("userId"); - - public QMemoId(String variable) { - super(MemoId.class, forVariable(variable)); - } - - public QMemoId(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QMemoId(PathMetadata metadata) { - super(MemoId.class, metadata); - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QPasswordResetToken.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QPasswordResetToken.java deleted file mode 100644 index b1c17280..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QPasswordResetToken.java +++ /dev/null @@ -1,61 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QPasswordResetToken is a Querydsl query type for PasswordResetToken - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QPasswordResetToken extends EntityPathBase { - - private static final long serialVersionUID = -1219280267L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QPasswordResetToken passwordResetToken = new QPasswordResetToken("passwordResetToken"); - - public final QBaseEntity _super = new QBaseEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath expiryDate = createDateTime("expiryDate", java.time.LocalDateTime.class); - - public final StringPath token = createString("token"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final QUser user; - - public QPasswordResetToken(String variable) { - this(PasswordResetToken.class, forVariable(variable), INITS); - } - - public QPasswordResetToken(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QPasswordResetToken(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QPasswordResetToken(PathMetadata metadata, PathInits inits) { - this(PasswordResetToken.class, metadata, inits); - } - - public QPasswordResetToken(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.user = inits.isInitialized("user") ? new QUser(forProperty("user"), inits.get("user")) : null; - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QResource.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QResource.java deleted file mode 100644 index 067289e9..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QResource.java +++ /dev/null @@ -1,60 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QResource is a Querydsl query type for Resource - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QResource extends EntityPathBase { - - private static final long serialVersionUID = 2055422878L; - - public static final QResource resource = new QResource("resource"); - - public final QBaseEntity _super = new QBaseEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath embeddedUrl = createString("embeddedUrl"); - - public final ListPath episodes = this.createList("episodes", CollectionEpisode.class, QCollectionEpisode.class, PathInits.DIRECT2); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath resourceDetails = createString("resourceDetails"); - - public final NumberPath runtime = createNumber("runtime", Integer.class); - - public final StringPath title = createString("title"); - - public final EnumPath type = createEnum("type", learningFlow.learningFlow_BE.domain.enums.ResourceType.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final StringPath url = createString("url"); - - public QResource(String variable) { - super(Resource.class, forVariable(variable)); - } - - public QResource(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QResource(PathMetadata metadata) { - super(Resource.class, metadata); - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java deleted file mode 100644 index 2ce1b859..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUser.java +++ /dev/null @@ -1,85 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QUser is a Querydsl query type for User - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QUser extends EntityPathBase { - - private static final long serialVersionUID = -1386433701L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QUser user = new QUser("user"); - - public final QBaseEntity _super = new QBaseEntity(this); - - public final DatePath birthDay = createDate("birthDay", java.time.LocalDate.class); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath email = createString("email"); - - public final EnumPath gender = createEnum("gender", learningFlow.learningFlow_BE.domain.enums.Gender.class); - - public final QImage image; - - public final BooleanPath inactive = createBoolean("inactive"); - - public final ListPath> interestFields = this.>createList("interestFields", learningFlow.learningFlow_BE.domain.enums.InterestField.class, EnumPath.class, PathInits.DIRECT2); - - public final EnumPath job = createEnum("job", learningFlow.learningFlow_BE.domain.enums.Job.class); - - public final StringPath loginId = createString("loginId"); - - public final StringPath name = createString("name"); - - public final EnumPath preferType = createEnum("preferType", learningFlow.learningFlow_BE.domain.enums.MediaType.class); - - public final StringPath providerId = createString("providerId"); - - public final StringPath pw = createString("pw"); - - public final EnumPath role = createEnum("role", learningFlow.learningFlow_BE.domain.enums.Role.class); - - public final EnumPath socialType = createEnum("socialType", learningFlow.learningFlow_BE.domain.enums.SocialType.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final ListPath userCollections = this.createList("userCollections", UserCollection.class, QUserCollection.class, PathInits.DIRECT2); - - public QUser(String variable) { - this(User.class, forVariable(variable), INITS); - } - - public QUser(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QUser(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QUser(PathMetadata metadata, PathInits inits) { - this(User.class, metadata, inits); - } - - public QUser(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.image = inits.isInitialized("image") ? new QImage(forProperty("image")) : null; - } - -} - diff --git a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUserCollection.java b/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUserCollection.java deleted file mode 100644 index ffa8f829..00000000 --- a/learningFlow/src/main/generated/learningFlow/learningFlow_BE/domain/QUserCollection.java +++ /dev/null @@ -1,58 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QUserCollection is a Querydsl query type for UserCollection - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QUserCollection extends EntityPathBase { - - private static final long serialVersionUID = -1884551079L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QUserCollection userCollection = new QUserCollection("userCollection"); - - public final QCollection collection; - - public final NumberPath id = createNumber("id", Long.class); - - public final DatePath lastAccessedAt = createDate("lastAccessedAt", java.time.LocalDate.class); - - public final QUser user; - - public final NumberPath userCollectionStatus = createNumber("userCollectionStatus", Integer.class); - - public QUserCollection(String variable) { - this(UserCollection.class, forVariable(variable), INITS); - } - - public QUserCollection(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QUserCollection(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QUserCollection(PathMetadata metadata, PathInits inits) { - this(UserCollection.class, metadata, inits); - } - - public QUserCollection(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.collection = inits.isInitialized("collection") ? new QCollection(forProperty("collection"), inits.get("collection")) : null; - this.user = inits.isInitialized("user") ? new QUser(forProperty("user"), inits.get("user")) : null; - } - -} - diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java deleted file mode 100644 index 4688bd1d..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java +++ /dev/null @@ -1,71 +0,0 @@ -package learningFlow.learningFlow_BE.apiPayload.code.status; - -import learningFlow.learningFlow_BE.apiPayload.code.BaseErrorCode; -import learningFlow.learningFlow_BE.apiPayload.code.ErrorReasonDTO; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum ErrorStatus implements BaseErrorCode { - - // For test - TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), - - // 가장 일반적인 응답 - _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), - _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."), - _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."), - _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), - - - // 멤버 관려 에러 - USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자를 찾을 수 없습니다."), - NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."), - - //Resources 관련 에어 - RESOURCES_NOT_FOUND(HttpStatus.NOT_FOUND,"RESOURCE4001","강의 에피소드를 찾을 수 없습니다."), - - //페이징 시 페이지 범위를 벗어났을 때 - PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, "PAGE4001","페이지 범위에 맞지 않는 페이지 값 입니다."), - - //컬렉션 관련 에러 - COLLECTION_NOT_FOUND(HttpStatus.NOT_FOUND,"COLLECTION4001","해당 컬렉션을 찾을 수 없습니다."), - - NO_MORE_COLLECTION(HttpStatus.NOT_FOUND,"COLLECTION4002","더 이상 컬렉션이 존재하지 않습니다."), - - //계정 로그인 관련 에러 - - // 예시,,, - ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."), - - - EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."), - INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."),; - - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public ErrorReasonDTO getReason() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(false) - .build(); - } - - @Override - public ErrorReasonDTO getReasonHttpStatus() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(false) - .httpStatus(httpStatus) - .build() - ; - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/redis/RedisConfig.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/redis/RedisConfig.java deleted file mode 100644 index 19903a01..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/redis/RedisConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package learningFlow.learningFlow_BE.config.redis; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new StringRedisSerializer()); - - return template; - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/SearchConverter.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/SearchConverter.java deleted file mode 100644 index 62ff59fe..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/SearchConverter.java +++ /dev/null @@ -1,58 +0,0 @@ -package learningFlow.learningFlow_BE.converter; - -import learningFlow.learningFlow_BE.domain.Collection; -import learningFlow.learningFlow_BE.domain.CollectionEpisode; -import learningFlow.learningFlow_BE.domain.Resource; -import learningFlow.learningFlow_BE.domain.enums.MediaType; -import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; -import learningFlow.learningFlow_BE.web.dto.search.SearchResponseDTO; - -import java.util.List; - -public class SearchConverter { - - public static SearchRequestDTO.SearchConditionDTO toSearchConditionDTO( - String keyword, - MediaType mediaType, - List difficulties, - List amounts - ) { - return SearchRequestDTO.SearchConditionDTO.builder() - .keyword(keyword) - .mediaType(mediaType) - .difficulties(difficulties) - .amounts(amounts) - .build(); - } - - public static SearchResponseDTO.SearchResultDTO toSearchResultDTO(List collections, Long lastId, boolean hasNext) { - List list - = collections.stream().map(SearchConverter::toCollectionPreviewDTO).toList(); - - return SearchResponseDTO.SearchResultDTO.builder() - .searchResults(list) - .lastId(lastId) - .hasNext(hasNext) - .build(); - } - - public static SearchResponseDTO.CollectionPreviewDTO toCollectionPreviewDTO(Collection collection) { - - int totalSeconds = collection.getEpisodes().stream() - .map(CollectionEpisode::getResource) - .mapToInt(Resource::getRuntime).sum(); - - int totalHours = (int) Math.ceil(totalSeconds / 3600); - - return SearchResponseDTO.CollectionPreviewDTO.builder() - .id(collection.getId()) - .title(collection.getTitle()) - .creator(collection.getCreator()) - .keywords(collection.getKeywords()) - .difficulties(collection.getDifficulty()) - .mediaType(collection.getMediaType()) - .amount(collection.getAmount()) - .runtime(totalHours) - .build(); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java deleted file mode 100644 index 40f248b3..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -package learningFlow.learningFlow_BE.converter; - -import learningFlow.learningFlow_BE.domain.User; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO.UserInfoDTO; -import org.springframework.stereotype.Component; - -@Component -public class UserConverter { - - public static UserResponseDTO.UserLoginResponseDTO toUserLoginResponseDTO(User user) { - - return UserResponseDTO.UserLoginResponseDTO.builder() - .loginId(user.getLoginId()) - .email(user.getEmail()) - .name(user.getName()) - .role(user.getRole()) - .socialType(user.getSocialType()) - .build(); - } - - public UserInfoDTO convertToUserInfoDTO(User user) { - return UserInfoDTO.builder() - .name(user.getName()) - .email(user.getEmail()) - .job(user.getJob()) - .interestFields(user.getInterestFields()) - .gender(user.getGender()) - .preferType(user.getPreferType()) - .profileImageUrl(user.getImage() != null ? user.getImage().getImageURL() : null) - .build(); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Image.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Image.java deleted file mode 100644 index d925689c..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Image.java +++ /dev/null @@ -1,36 +0,0 @@ -package learningFlow.learningFlow_BE.domain; - -import jakarta.persistence.*; -import lombok.*; - -import java.math.BigInteger; -import java.util.List; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Entity -@Table(name = "image") -public class Image { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String imageURL; - - @Column(nullable = false) - private String fileType; - - @Column(nullable = false) - private BigInteger fileSize; - - @OneToMany(mappedBy = "image", cascade = CascadeType.ALL) - private List users; - - @OneToMany(mappedBy = "image", cascade = CascadeType.ALL) - private List collections; - -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Gender.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Gender.java deleted file mode 100644 index e2ed95d5..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Gender.java +++ /dev/null @@ -1,5 +0,0 @@ -package learningFlow.learningFlow_BE.domain.enums; - -public enum Gender { - MALE, FEMALE -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java deleted file mode 100644 index 8e571c0d..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package learningFlow.learningFlow_BE.repository; - -import learningFlow.learningFlow_BE.domain.Image; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ImageRepository extends JpaRepository { -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java deleted file mode 100644 index 6262682d..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package learningFlow.learningFlow_BE.security.handler; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import learningFlow.learningFlow_BE.security.jwt.JwtTokenProvider; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import java.util.concurrent.TimeUnit; - -@Slf4j -@Component -@RequiredArgsConstructor -public class JwtLogoutHandler implements LogoutHandler { - - private final RedisTemplate redisTemplate; - private final JwtTokenProvider jwtTokenProvider; - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - String jwt = getJwtFromRequest(request); - addToBlacklist(jwt); - } - - public void addToBlacklist(String token) { - log.info("토큰 블랙 리스트 추가 시도"); - - if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { - log.info("유효한 토큰 발견: {}", token); - long expiration = jwtTokenProvider.getExpirationFromToken(token); - long now = System.currentTimeMillis(); - long remainingTime = (expiration - now) / 1000; - - redisTemplate.opsForValue() - .set("BLACKLIST:" + token, "true", remainingTime, TimeUnit.SECONDS); - log.info("토큰이 블랙리스트에 추가됨"); - } else { - log.info("유효하지 않은 토큰"); - throw new RuntimeException("유효하지 않은 토큰입니다."); - } - } - - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java deleted file mode 100644 index c8fb16f1..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java +++ /dev/null @@ -1,147 +0,0 @@ -package learningFlow.learningFlow_BE.security.jwt; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.service.user.CustomUserDetailsService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtTokenProvider jwtTokenProvider; - private final CustomUserDetailsService customUserDetailsService; - private final RedisTemplate redisTemplate; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - - log.info("JWT 필터 진입, URL: {}", request.getRequestURI()); - - /** - * 로그인, 회원가입 등 인증이 필요없는 경로는 토큰 검증을 건너뛰도록 설정 - * 인증 미필요 URL 체크 - */ - if (isPermitAllUrl(request.getRequestURI())) { - log.info("인증이 필요없는 URL: {}", request.getRequestURI()); - filterChain.doFilter(request, response); - return; - } - - try { - String jwt = getJwtFromRequest(request); - log.info("요청에서 추출한 토큰: {}", jwt); - - if (StringUtils.hasText(jwt)) { - //AccessToken 검증 - if (jwtTokenProvider.validateToken(jwt)) { - log.info("유효한 Access Token"); - - //로그아웃 요청이 아닌 경우에만 블랙리스트 체크 - if (!request.getRequestURI().equals("/logout/test")) { - //TODO : 테스트 위해서 URI 설정을 /logout/test로 해놓음. 추후 수정 필요 - //Redis에서 블랙리스트 체크하기 - 로그아웃된 사용자인지 여부 파악 - Boolean isBlacklisted = redisTemplate.hasKey("BLACKLIST:" + jwt); - log.info("블랙리스트 체크 결과: {}", isBlacklisted); - - if (Boolean.TRUE.equals(isBlacklisted)) { - throw new RuntimeException("이미 로그아웃된 토큰입니다."); - } - } - - //유효한 토큰이면 인증 처리 - String email = jwtTokenProvider.getEmailFromToken(jwt); - log.info("토큰에서 추출한 이메일: {}", email); - - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - UsernamePasswordAuthenticationToken authentication - = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - SecurityContextHolder.getContext().setAuthentication(authentication); - log.info("인증 정보 SecurityContext에 저장"); - } else { - log.info("Access Token이 만료되어 Refresh Token 확인을 시도"); - // Access Token이 만료된 경우, Refresh Token 확인 - String refreshToken = request.getHeader("Refresh-Token"); - log.info("전달받은 Refresh Token: {}", refreshToken); - - if (!StringUtils.hasText(refreshToken)) { - log.info("Refresh Token이 없음 - 재로그인 필요"); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - String error = new ObjectMapper().writeValueAsString( - ApiResponse.onFailure("401", "토큰이 만료되었습니다. 다시 로그인해주세요.", null) - ); - response.getWriter().write(error); - return; - } - - if (jwtTokenProvider.validateToken(refreshToken)) { - log.info("유효한 Refresh Token. 새로운 Access Token을 발급 시작"); - - // Refresh Token이 유효하면 새로운 Access Token 발급 - String email = jwtTokenProvider.getEmailFromToken(refreshToken); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - - UsernamePasswordAuthenticationToken authentication - = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - - //새로운 AccessToken 발급 및 헤더에 추가 - String newAccessToken = jwtTokenProvider.createAccessToken(authentication); - log.info("새로 발급된 Access Token: {}", newAccessToken); - response.addHeader("Authorization", "Bearer " + newAccessToken); - - //새로운 토큰으로 인증 처리 - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); - log.info("새로운 Access Token으로 인증 정보를 업데이트 완료"); - } - } - } - } catch (Exception e) { - log.error("Security Context에서 사용자 인증을 설정할 수 없습니다.", e); - } - - filterChain.doFilter(request, response); - } - - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } - - private boolean isPermitAllUrl(String requestURI) { - return requestURI.equals("/login") || - requestURI.equals("/register") || - requestURI.startsWith("/register/complete") || - requestURI.equals("/login/google") || - requestURI.startsWith("/oauth2") || - requestURI.startsWith("/swagger-ui") || - requestURI.startsWith("/v3/api-docs") || - requestURI.startsWith("/swagger-resources") || - requestURI.startsWith("/webjars") || - requestURI.startsWith("/find") || - requestURI.equals("/reset-password"); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java deleted file mode 100644 index 9bac0296..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java +++ /dev/null @@ -1,61 +0,0 @@ -package learningFlow.learningFlow_BE.service.auth.common; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -@Slf4j -public class UserVerificationEmailService { - - private final JavaMailSender emailSender; - - @Value("${app.url}") - private String baseUrl; - - public void sendVerificationEmail(String email, String token) { - try { - SimpleMailMessage message = new SimpleMailMessage(); - message.setTo(email); - message.setSubject("[OnBoarding] 이메일 인증"); - message.setText( - "안녕하세요, OnBoarding입니다.\n\n" + - "회원가입을 완료하기 위해 아래 링크를 클릭하여 추가 정보를 입력해주세요:\n\n" + - baseUrl + "/register/complete?token=" + token + "\n\n" + - "이 링크는 24시간 동안 유효합니다.\n" + - "본인이 요청하지 않은 경우 이 이메일을 무시해주세요." - ); - - emailSender.send(message); - log.info("이메일 인증 메일 발송 완료: {}", email); - } catch (Exception e) { - log.error("이메일 발송 실패: {}", e.getMessage()); - throw new RuntimeException("이메일 발송에 실패했습니다."); - } - } - - public void sendPasswordResetEmail(String email, String token) { - try { - SimpleMailMessage message = new SimpleMailMessage(); - message.setTo(email); - message.setSubject("[OnBoarding] 비밀번호 재설정"); - message.setText( - "안녕하세요, OnBoarding입니다.\n\n" + - "비밀번호 재설정을 위해 아래 링크를 클릭해주세요:\n\n" + - baseUrl + "/reset-password?token=" + token + "\n\n" + - "이 링크는 24시간 동안 유효합니다.\n" + - "본인이 요청하지 않은 경우 이 이메일을 무시해주세요." - ); - - emailSender.send(message); - log.info("비밀번호 재설정 이메일 발송 완료: {}", email); - } catch (Exception e) { - log.error("이메일 발송 실패: {}", e.getMessage()); - throw new RuntimeException("이메일 발송에 실패했습니다."); - } - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/search/SearchService.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/search/SearchService.java deleted file mode 100644 index 0d9c7757..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/search/SearchService.java +++ /dev/null @@ -1,46 +0,0 @@ -package learningFlow.learningFlow_BE.service.search; - -import learningFlow.learningFlow_BE.converter.SearchConverter; -import learningFlow.learningFlow_BE.domain.Collection; -import learningFlow.learningFlow_BE.repository.search.SearchRepositoryCustom; -import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; -import learningFlow.learningFlow_BE.web.dto.search.SearchResponseDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class SearchService { - - private final SearchRepositoryCustom searchRepository; - private static final int PAGE_SIZE = 8; - - public SearchResponseDTO.SearchResultDTO search(SearchRequestDTO.SearchConditionDTO condition, Long lastId) { - - PageRequest pageRequest = PageRequest.of(0, PAGE_SIZE); - List collections = searchRepository.searchCollections(condition, lastId, pageRequest); - - - if (collections.isEmpty()) { - return SearchConverter.toSearchResultDTO(collections, null, false); - } - - Long lastCollectionId = collections.getLast().getId(); - boolean hasNext = hasNextPage(condition,lastCollectionId); - - return SearchConverter.toSearchResultDTO(collections, lastCollectionId, hasNext); - } - - private boolean hasNextPage(SearchRequestDTO.SearchConditionDTO condition, long lastCollectionId) { - List nextCollections = searchRepository.searchCollections( - condition, - lastCollectionId, - PageRequest.of(0, 1) - ); - - return !nextCollections.isEmpty(); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/user/UserService.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/user/UserService.java deleted file mode 100644 index 502d2292..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/user/UserService.java +++ /dev/null @@ -1,55 +0,0 @@ -package learningFlow.learningFlow_BE.service.user; - -import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; -import learningFlow.learningFlow_BE.apiPayload.exception.handler.UserHandler; -import learningFlow.learningFlow_BE.converter.UserConverter; -import learningFlow.learningFlow_BE.domain.User; -import learningFlow.learningFlow_BE.repository.UserRepository; -import learningFlow.learningFlow_BE.web.dto.user.UserRequestDTO.UpdateUserDTO; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO.UserInfoDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class UserService { - - private final UserRepository userRepository; - private final UserConverter userConverter; - - public UserInfoDTO getUserInfo(String loginId) { - User user = userRepository.findById(loginId) - .orElseThrow(() -> new UserHandler(ErrorStatus.USER_NOT_FOUND)); - - return userConverter.convertToUserInfoDTO(user); - } - - @Transactional - public UserInfoDTO updateUserInfo(String loginId, UpdateUserDTO updateUserDTO, MultipartFile imageFile) { - User user = userRepository.findById(loginId) - .orElseThrow(() -> new UserHandler(ErrorStatus.USER_NOT_FOUND)); - - // TODO: 이미지 업데이트 로직 추가 예정 - if (imageFile != null && !imageFile.isEmpty()) { - log.info("이미지 업데이트 요청 발생 - 추후 구현 예정"); - } - - // 각 필드가 null이 아닌 경우에만 업데이트 - if (updateUserDTO.getName() != null) { - user.updateName(updateUserDTO.getName()); - } - if (updateUserDTO.getJob() != null) { - user.updateJob(updateUserDTO.getJob()); - } - if (updateUserDTO.getInterestFields() != null && !updateUserDTO.getInterestFields().isEmpty()) { - user.updateInterestFields(updateUserDTO.getInterestFields()); - } - - return getUserInfo(loginId); - } -} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/CollectionRestController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/CollectionRestController.java deleted file mode 100644 index a6893644..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/CollectionRestController.java +++ /dev/null @@ -1,44 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/collections") -@Validated -@Slf4j -@Tag(name = "Collection", description = "특정 컬렉션 조회할 수 있는 API") -public class CollectionRestController { - - @GetMapping("{collection-id}") - @Operation(summary = "컬렉션 조회 API", description = "특정 컬렉션을 조회하는 API입니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COLLECTION4001", description = "존재하지 않는 컬렉션입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - @Parameters({ - @Parameter(name = "collection-id", description = "컬렉션 ID"), - }) - public ApiResponse getCollection( - @PathVariable("collection-id") Long collectionId - ) { - // TODO: 컬렉션 조회 로직 구현 - return ApiResponse.onSuccess(null); - } - -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/HomeRestController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/HomeRestController.java deleted file mode 100644 index 58c23fd8..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/HomeRestController.java +++ /dev/null @@ -1,74 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; -import learningFlow.learningFlow_BE.web.dto.home.HomeResponseDTO.HomeInfoDTO; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static learningFlow.learningFlow_BE.converter.UserConverter.toUserLoginResponseDTO; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/home") -@Validated -@Slf4j -@Tag(name = "Home", description = "홈 화면 보여주는 API") -public class HomeRestController { - - - @GetMapping - @Operation(summary = "홈 화면 API", description = "홈 화면에 필요한 정보를 제공하는 API입니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER4001", description = "사용자를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - public ApiResponse getHome( -// @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - // TODO : 추후 구현될 로직을 위한 주석 - - /** - * 1. 현재 로그인한 사용자 정보 조회 - * 2. 사용자 기반 추천 컬렉션 목록 조회 - * 3. 사용자의 최근 수강 컬렉션 목록 조회 - * 4. HomeInfoDto로 변환하여 반환 - */ - - /** - * Spring Security 사용하기 때문에 @AuthenticationPrincipal를 통해 바로 현재 인증된 사용자의 정보를 가져올 수 있음 - */ - -// User user = principalDetails.getUser(); - - return ApiResponse.onSuccess(null); - } - - - @GetMapping("/test") - @Operation(summary = "홈 화면 테스트용 API", description = "로그인, 로그아웃 상태 유지되는지 확인할 수 있는 API") - public ApiResponse getHomeTest( - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - log.info("/home/test 시작"); - - if (principalDetails != null) { - log.info("인증된 사용자: {}", principalDetails.getUsername()); - return ApiResponse.onSuccess(toUserLoginResponseDTO(principalDetails.getUser())); - } - - log.info("인증되지 않은 사용자"); - return ApiResponse.onSuccess(null); - } -} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/LoginController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/LoginController.java deleted file mode 100644 index df9cb7d2..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/LoginController.java +++ /dev/null @@ -1,155 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import learningFlow.learningFlow_BE.service.auth.local.LocalUserAuthService; -import learningFlow.learningFlow_BE.service.auth.oauth.OAuth2UserRegistrationService; -import learningFlow.learningFlow_BE.web.dto.user.UserRequestDTO; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -import java.io.IOException; - -@RestController -@RequiredArgsConstructor -@RequestMapping -@Tag(name = "Login", description = "회원가입, 로그인 관련 API") -@Slf4j -public class LoginController { - - private final LocalUserAuthService localUserAuthService; - private final OAuth2UserRegistrationService OAuth2UserRegistrationService; - - @PostMapping("/register") - @Operation(summary = "회원가입 초기 단계 API", description = "이메일과 비밀번호를 입력받아 인증 이메일을 발송하는 API") - public ApiResponse register( - @Valid @RequestBody UserRequestDTO.InitialRegisterDTO request - ) { - localUserAuthService.initialRegister(request); - return ApiResponse.onSuccess("인증 이메일이 발송되었습니다. 이메일을 확인해주세요."); - } - - @GetMapping("/register/complete") - @Operation(summary = "회원가입 완료 API", description = "이메일 인증 후 추가 정보를 입력받아 회원가입을 완료하는 API") - public ApiResponse goCompleteRegister( - @RequestParam String token - ) { - localUserAuthService.validateRegistrationToken(token); - return ApiResponse.onSuccess("토큰이 유효. 추가 정보를 입력해주세요."); - } - - @PostMapping("/register/complete") - @Operation(summary = "회원가입 완료 API", description = "이메일 인증 후 추가 정보를 입력받아 회원가입을 완료하는 API") - public ApiResponse completeRegister( - @RequestParam String token, - @Valid @RequestBody UserRequestDTO.CompleteRegisterDTO request, - HttpServletResponse response - ) { - return ApiResponse.onSuccess(localUserAuthService.completeRegister(token, request, response)); - //TODO: 회원가입 후 로그인 창으로 리다이렉트 하는게 나을것 같은데 이 부분은 아직 설정 안함(리다이렉트 설정 시 스웨거 테스트 불편) - } - - @PostMapping("/login") - @Operation(summary = "일반 로그인 API", description = "이메일과 비밀번호를 통한 일반 로그인을 처리하는 API") - public ApiResponse login( - @Valid @RequestBody UserRequestDTO.UserLoginDTO request, - HttpServletResponse response - ) { - log.info("/login 시작"); - return ApiResponse.onSuccess(localUserAuthService.login(request, response)); - //TODO: 로그인 후에도 /home으로 리다이렉트 되는게 나을 것 같은데 이 부분 설정 안함(리다이렉트 설정 시 스웨거 테스트 불편) - } - - /** - * localhost:8080/oauth2/authorization/google로 리다이렉트해서 로그인 수행 - * 이미 존재하는 회원인 경우 로그인, 존재하지 않는 회원인 경우 getAdditionalInfoPage() 호출하면서 회원가입 진행 - */ - @GetMapping("/login/google") - @Operation(summary = "구글 로그인 리다이렉트", description = "구글 로그인 페이지로 리다이렉트하는 API") - public void googleLogin( - HttpServletResponse response - ) throws IOException { - response.sendRedirect("/oauth2/authorization/google"); - } - - /** - * 만약에 기존에 존재하던 회원이 아닌 경우에는 - * 우선 구글에서 받아올 수 있는 정보들만으로 임시회원 oauth2UserTemp를 생성해서 세션에 저장하고 - * getAdditionalInfoPage() 호출해서 프론트가 - * 회원가입에 필요하지만 구글에서 제공하지 않는 추가 필드 입력하는 페이지로 리다이렉트 한 다음에 해당 정보 전달해주면, - * updateAdditionalInfo()에서 그 데이터 받아서 회원가입 완료 - */ - @GetMapping("/oauth2/additional-info") - @Operation(summary = "추가 정보 입력 페이지", description = "OAuth2 회원가입 후 추가 정보 입력이 필요한 경우 리다이렉트되는 엔드포인트") - public ApiResponse getAdditionalInfoPage() { - log.info("get info"); - return ApiResponse.onSuccess(OAuth2UserRegistrationService.getAdditionalInfoRequirements()); - } - - /** - * updateAdditionalInfo()에서 프론트에서 전달한 추가 데이터와 세션에 저장된 oauth2UserTemp에 - * 추가 데이터를 저장해서 회원가입 완료. - * 그리고 회원가입 완료되면 세션에 저장된 oauth2UserTemp는 삭제하고 인증 정보를 SecurityContextHolder에 추가해서 인증이 가능하게 한다. - * @return UserResponseDTO.UserLoginResponseDTO - */ - @PutMapping("/oauth2/additional-info") - @Operation(summary = "추가 정보 입력 API", description = "OAuth2 회원가입 후 추가 정보를 입력하는 API") - public ApiResponse updateAdditionalInfo( - @RequestParam String token, - @Valid @RequestBody UserRequestDTO.AdditionalInfoDTO request, - HttpServletResponse response) { - log.info("put info"); - return ApiResponse.onSuccess(OAuth2UserRegistrationService.updateAdditionalInfo(token, request, response)); - } - - @PostMapping("/find/password") - @Operation(summary = "비밀번호 재설정 요청 API", description = "이메일을 통해 비밀번호 재설정 링크를 전송하는 API") - public ApiResponse requestPasswordReset( - @Valid @RequestBody UserRequestDTO.FindPasswordDTO request - ) { - localUserAuthService.sendPasswordResetEmail(request); - return ApiResponse.onSuccess("이메일이 성공적으로 발송되었습니다."); - } - - @PostMapping("/reset-password") - @Operation(summary = "비밀번호 재설정 API", description = "이메일로 받은 토큰을 통해 비밀번호를 재설정하는 API") - public ApiResponse resetPassword( - @Valid @RequestBody UserRequestDTO.ResetPasswordDTO request - ) { - localUserAuthService.resetPassword(request); - return ApiResponse.onSuccess("비밀번호 재설정이 완료되었습니다."); - } - - @PostMapping("/logout") - @Operation(summary = "로그아웃 API", description = "로그아웃 실행하는 API, 실행 후 홈 화면으로 리다이렉트") - public String logout( - HttpServletRequest request - ) { - String token = request.getHeader("Authorization"); - localUserAuthService.logout(token); - return "redirect:/home"; - } - - @PostMapping("/logout/test") // 테스트 전용 API - @Operation(summary = "로그아웃 테스트 용 API", description = "로그 아웃 테스트 후 리다이렉트 수행 안하고 스웨거에서 확인 가능하게 문자열 출력해주는 API") - public ApiResponse testLogout( - HttpServletRequest request - ) { - log.info("로그아웃 요청 받음, /logout/test 시작"); - try { - String token = request.getHeader("Authorization"); - log.info("받은 토큰: {}", token); - localUserAuthService.logout(token); - return ApiResponse.onSuccess("로그아웃 성공"); - } catch (Exception e) { - log.error("로그아웃 처리 중 오류: {}", e.getMessage()); - throw new RuntimeException("로그아웃 처리 중 오류가 발생했습니다."); - } - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/ResourceRestController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/ResourceRestController.java deleted file mode 100644 index d5d6b421..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/ResourceRestController.java +++ /dev/null @@ -1,60 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.web.dto.memo.MemoRequestDTO; -import learningFlow.learningFlow_BE.web.dto.memo.MemoResponseDTO; -import learningFlow.learningFlow_BE.web.dto.resource.ResourceResponseDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -@Validated -@RequestMapping("/resources") -@Slf4j -@Tag(name = "Resource", description = "Collection 내에 특정 resource 관련해서 기능하는 API") -public class ResourceRestController { - - @GetMapping("/{episode-id}") - @Operation(summary = "강의 시청, 강좌로 이동 API", description = "강의 에피소드를 시청하기 위해 강좌로 이동하는 API, 그리고 강의를 시청 처리하는 로직도 포함") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "RESOURCE4001", description = "강의 에피소드를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - @Parameters({ - @Parameter(name = "episode-id", description = "시청할 강의 에피소드 ID") - }) - public ApiResponse watchEpisode(@PathVariable("episode-id") Long episodeId) { - /** - * 강의 시청 하는 API로 강좌로 이동하는 API 이기 때문에 일단 Resource의 Url을 반환하게 해놓았어요. - */ - // TODO: 강의 시청 로직 구현, 반환 DTO로 converting - return ApiResponse.onSuccess(null); - } - - @PostMapping("/{episode-id}/memo") - @Operation(summary = "강의 메모 생성 API", description = "강의 에피소드에 메모를 추가하는 API") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "RESOURCE4001", description = "강의 에피소드를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - @Parameters({ - @Parameter(name = "episode-id", description = "메모를 추가할 강의 에피소드 ID") - }) - public ApiResponse createMemo(@PathVariable("episode-id") Long episodeId, @RequestBody MemoRequestDTO.MemoJoinDTO request) { - /** - * 메모를 생성하고 저장하게 되면 자신이 쓴 메모를 보여주는게 맞을 것 같아서 일단 메모 contents를 반환하게 해놓았어요. - */ - // TODO: 강의 메모 생성 로직 구현 - return ApiResponse.onSuccess(null); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/SearchRestController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/SearchRestController.java deleted file mode 100644 index 3b938b74..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/SearchRestController.java +++ /dev/null @@ -1,60 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.converter.SearchConverter; -import learningFlow.learningFlow_BE.domain.enums.MediaType; -import learningFlow.learningFlow_BE.service.search.SearchService; -import learningFlow.learningFlow_BE.web.dto.search.SearchResponseDTO; -import learningFlow.learningFlow_BE.domain.enums.MediaType; -import learningFlow.learningFlow_BE.validation.annotation.CheckPage; -import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/search") -@Validated -@Slf4j -@Tag(name = "Search", description = "검색 API") -public class SearchRestController { - - private final SearchService searchService; - - @GetMapping - @Operation(summary = "강의 검색 API", description = "키워드로 강의 에피소드를 검색하는 API") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON400", description = "잘못된 요청입니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - @Parameters({ - @Parameter(name = "keyword", description = "검색어 (컬렉션 제목, 크리에이터, 키워드, 에피소드 제목)"), - @Parameter(name = "mediaType", description = "미디어 타입 필터 (VIDEO, TEXT)"), - @Parameter(name = "difficulties", description = "난이도 필터 (1: 입문, 2: 초급, 3: 중급, 4: 실무)"), - @Parameter(name = "amounts", description = "강의량 필터 (SHORT(1-5), MEDIUM(5-10), LONG(11이상)"), - @Parameter(name = "lastId", description = "마지막으로 조회된 컬렉션의 ID (첫 페이지는 0)") - }) - public ApiResponse searchEpisodes( - @RequestParam(required = false) String keyword, - @RequestParam(required = false) MediaType mediaType, - @RequestParam(required = false) List difficulties, - @RequestParam(required = false) List amounts, - @RequestParam(required = false, defaultValue = "0") Long lastId - ) { - return ApiResponse.onSuccess(searchService.search(SearchConverter.toSearchConditionDTO(keyword, mediaType, difficulties, amounts), lastId)); - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/UserRestController.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/UserRestController.java deleted file mode 100644 index 52720479..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/controller/UserRestController.java +++ /dev/null @@ -1,60 +0,0 @@ -package learningFlow.learningFlow_BE.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; -import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; -import learningFlow.learningFlow_BE.service.user.UserService; -import learningFlow.learningFlow_BE.web.dto.user.UserRequestDTO.UpdateUserDTO; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO.UserInfoDTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -@RestController -@RequiredArgsConstructor -@Validated -@RequestMapping("/user") -@Slf4j -@Tag(name = "User", description = "사용자 관련 API") -public class UserRestController { - - private final UserService userService; - - @PutMapping - @Operation(summary = "사용자 정보 수정 API", description = "사용자 정보를 수정하는 API") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER4001", description = "사용자를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - public ApiResponse updateUserInfo( - @Valid @RequestBody UpdateUserDTO updateUserDTO, - @RequestPart(value = "image", required = false) MultipartFile imageFile, - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - return ApiResponse.onSuccess( - userService.updateUserInfo(principalDetails.getUser().getLoginId(), updateUserDTO, imageFile) - ); - } - - @GetMapping - @Operation(summary = "사용자 정보 조회 API", description = "로그인한 사용자의 정보를 조회하는 API") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "USER4001", description = "사용자를 찾을 수 없습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - }) - public ApiResponse getUserInfo( - @AuthenticationPrincipal PrincipalDetails principalDetails - ) { - return ApiResponse.onSuccess( - userService.getUserInfo(principalDetails.getUser().getLoginId()) - ); - } -} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/collection/CollectionResponseDTO.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/collection/CollectionResponseDTO.java deleted file mode 100644 index af805278..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/collection/CollectionResponseDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package learningFlow.learningFlow_BE.web.dto.collection; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -public class CollectionResponseDTO { - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class CollectionListDto { - List collectionList; - Integer listSize; - boolean isFirst; - boolean isLast; - Integer totalPage; - Long totalElements; - } - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class CollectionDto { - String title; - // TODO: collection 조회 시 조회할 필드 들 구체적으로 정해서 넣어야 합니다. - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceRequestDTO.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceRequestDTO.java deleted file mode 100644 index e20d6bbd..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceRequestDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package learningFlow.learningFlow_BE.web.dto.resource; - -public class ResourceRequestDTO { -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceResponseDTO.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceResponseDTO.java deleted file mode 100644 index 96d0e9f5..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/resource/ResourceResponseDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package learningFlow.learningFlow_BE.web.dto.resource; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class ResourceResponseDTO { - - @Getter - @AllArgsConstructor - @NoArgsConstructor - @Builder - public static class ResourceUrlDTO { - String embeddedUrl; - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/search/SearchResponseDTO.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/search/SearchResponseDTO.java deleted file mode 100644 index 02c9f97e..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/search/SearchResponseDTO.java +++ /dev/null @@ -1,37 +0,0 @@ -package learningFlow.learningFlow_BE.web.dto.search; - -import learningFlow.learningFlow_BE.domain.enums.MediaType; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -public class SearchResponseDTO { - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class SearchResultDTO { - List searchResults; - Long lastId; // 마지막 컬렉션의 ID - Boolean hasNext; // 다음 페이지 존재 여부 - } - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class CollectionPreviewDTO { - Long id; - String title; - String creator; - List keywords; - List difficulties; - MediaType mediaType; - Integer amount; - Integer runtime; - } -} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/user/UserResponseDTO.java b/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/user/UserResponseDTO.java deleted file mode 100644 index fa93ce48..00000000 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/web/dto/user/UserResponseDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package learningFlow.learningFlow_BE.web.dto.user; - -import learningFlow.learningFlow_BE.domain.enums.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; -import java.util.List; - - -public class UserResponseDTO { - - @Getter - @AllArgsConstructor - @NoArgsConstructor - @Builder - public static class UserInfoDTO { - private String name; - private String email; - private Job job; - private List interestFields; - private LocalDate birthDay; - private Gender gender; - private MediaType preferType; - private String profileImageUrl; - /** - * 어떤 필드들을 사용자 정보 조회 시에 보여줘야 할지 아직 안정해서 비워두었습니다. - */ - // TODO: 사용자 정보 조회 시 DTO를 통해 보여줄 필드 정하기 - } - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class UserLoginResponseDTO { - private String loginId; - private String email; - private String name; - private Role role; - private SocialType socialType; - } -} diff --git a/learningFlow/src/main/resources/application.yml b/learningFlow/src/main/resources/application.yml deleted file mode 100644 index ae85686d..00000000 --- a/learningFlow/src/main/resources/application.yml +++ /dev/null @@ -1,55 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://localhost:3307/learningFlow_db # Docker?? ?? ?? MySQL ?? - username: learningFlow # Docker Compose?? ??? ??? ?? - password: learningFlow123 # Docker Compose?? ??? ???? - driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC ???? - jpa: - hibernate: - ddl-auto: update # ??? ?? ???? (?? ????? ??) - show-sql: true # SQL ?? ?? - sql: - init: - mode: always # SQL ??? ?? - security: - oauth2: - client: - registration: - google: - client-id: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT-ID} - client-secret: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT-SECRET} - scope: - - email - - profile - data: - redis: - host: localhost - port: 6379 - - mail: - host: smtp.gmail.com - port: 587 - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} - properties: - mail: - smtp: - auth: true - starttls: - enable: true - transport: - protocol: smtp - -# JWT 설정 추가 -custom: - jwt: - secretKey: ${JWT_SECRET_KEY} - access-token-validity-in-seconds: 3600 # Access Token 1시간 - refresh-token-validity-in-seconds: 604800 # Refresh Token 1주일 - -app: - url: http://localhost:8080 - -server: - port: 8080 # Spring Boot ?????? ?? ?? - diff --git a/learningFlow/settings.gradle b/settings.gradle similarity index 100% rename from learningFlow/settings.gradle rename to settings.gradle diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java b/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java similarity index 90% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java rename to src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java index 3d9123a8..0bd7a76b 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java +++ b/src/main/java/learningFlow/learningFlow_BE/LearningFlowBeApplication.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Import; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/ApiResponse.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/ApiResponse.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/ApiResponse.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/ApiResponse.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseCode.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseCode.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseCode.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseCode.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseErrorCode.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseErrorCode.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseErrorCode.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/code/BaseErrorCode.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ErrorReasonDTO.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ErrorReasonDTO.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ErrorReasonDTO.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ErrorReasonDTO.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ReasonDTO.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ReasonDTO.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ReasonDTO.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/code/ReasonDTO.java diff --git a/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java new file mode 100644 index 00000000..d74127ce --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java @@ -0,0 +1,103 @@ +package learningFlow.learningFlow_BE.apiPayload.code.status; + +import learningFlow.learningFlow_BE.apiPayload.code.BaseErrorCode; +import learningFlow.learningFlow_BE.apiPayload.code.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + + // For test + TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), + + // 가장 일반적인 응답 + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + + + // 멤버 관려 에러 + EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."), + EMAIL_VERIFICATION_IN_PROGRESS(HttpStatus.BAD_REQUEST, "EMAIL4002", "이미 진행 중인 이메일 인증이 있습니다. 이메일을 확인해주세요."), + EMAIL_CHANGE_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "EMAIL4004", "기존과 동일한 이메일로는 변경하실 수 없습니다."), + GOOGLE_USER_CANNOT_CHANGE_EMAIL(HttpStatus.BAD_REQUEST,"EMAIL4005","구글 로그인 유저는 이메일 변경을 하실 수 없습니다."), + EMAIL_CODE_INVALID(HttpStatus.BAD_REQUEST, "EMAIL4006", "유효하지 않은 이메일 인증 코드입니다."), + EMAIL_CODE_EXPIRED(HttpStatus.BAD_REQUEST,"EMAIL4007","만료된 이메일 인증 코드입니다. 이메일 인증을 다시 요청해주세요."), + + USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자를 찾을 수 없습니다."), + NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."), + WITHDRAWAL_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "USER5001", "계정 탈퇴 처리 중 오류가 발생했습니다."), + + // 비밀번호 관련 에러 추가 + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."), + PASSWORD_CURRENT_MISMATCH(HttpStatus.BAD_REQUEST, "PASSWORD4002", "현재 비밀번호가 일치하지 않습니다."), + PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "PASSWORD4003", "새 비밀번호는 현재 비밀번호와 달라야 합니다."), + PASSWORD_RESET_CODE_INVALID(HttpStatus.BAD_REQUEST, "PASSWORD4004", "유효하지 않은 비밀번호 재설정 코드입니다."), + PASSWORD_RESET_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "PASSWORD4005", "만료된 비밀번호 재설정 코드입니다. 비밀번호 재설정을 다시 요청해주세요."), + + + //Resources 관련 에어 + RESOURCES_NOT_FOUND(HttpStatus.NOT_FOUND,"RESOURCE4001","강의 에피소드를 찾을 수 없습니다."), + QUANTITY_IS_NULL(HttpStatus.BAD_REQUEST, "RESOURCE4002", "분량이 존재하지 않습니다"), + //페이징 시 페이지 범위를 벗어났을 때 + PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, "PAGE4001","페이지 범위에 맞지 않는 페이지 값 입니다."), + + //컬렉션 관련 에러 + COLLECTION_NOT_FOUND(HttpStatus.NOT_FOUND,"COLLECTION4001","해당 컬렉션을 찾을 수 없습니다."), + + NO_MORE_COLLECTION(HttpStatus.NOT_FOUND,"COLLECTION4002","더 이상 컬렉션이 존재하지 않습니다."), + + //계정 로그인 관련 에러 + LOGIN_REQUIRED(HttpStatus.UNAUTHORIZED, "AUTH4001", "로그인이 필요한 서비스입니다."), + + // 예시,,, + ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."), + + //이미지 + IMAGE_FORMAT_BADREQUEST(HttpStatus.BAD_REQUEST,"COMMON400","이미지 파일만 업로드할 수 있습니다."), + IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5001", "이미지 업로드에 실패했습니다. 다시 시도해주세요."), + + // 컬렉션 에피소드 에러 + EPISODE_NOT_FOUND(HttpStatus.NOT_FOUND, "EPISODE4001", "존재하지 않는 에피소드 입니다."), + + //URI 에러 + URI_SYNTAX_ERROR(HttpStatus.BAD_REQUEST, "URI4001", "잘못된 형태의 URI 입니다"), + // 임베드 에러 + YOUTUBE_URI_SYNTAX_ERROR(HttpStatus.BAD_REQUEST, "EMBED4001", "유튜브 URI 형식이 아닙니다."), + UNSUPPORTED_BLOG_PLATFORM(HttpStatus.BAD_REQUEST, "EMBED4002", "지원하지 않는 블로그 플랫폼입니다."), + BLOG_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "PORXY5001", "블로그 데이터를 가져오는 과정에서 오류가 발생했습니다."), + + + // Resource 에러 + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "RESOURCE4001", "존재하지 않는 리소스 입니다."), + + // User Progress + USER_PROGRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "USER-PROGRESS4001", "진도 값이 null 인 상태입니다."); + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build() + ; + } +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/SuccessStatus.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/SuccessStatus.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/SuccessStatus.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/SuccessStatus.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java similarity index 99% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java index 88b7a568..25ccb12c 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java +++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/ExceptionAdvice.java @@ -26,7 +26,6 @@ @RestControllerAdvice(annotations = {RestController.class}) public class ExceptionAdvice extends ResponseEntityExceptionHandler { - @ExceptionHandler public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { String errorMessage = e.getConstraintViolations().stream() diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/GeneralException.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/GeneralException.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/GeneralException.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/GeneralException.java diff --git a/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/CollectionHandler.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/CollectionHandler.java new file mode 100644 index 00000000..33babf94 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/CollectionHandler.java @@ -0,0 +1,10 @@ +package learningFlow.learningFlow_BE.apiPayload.exception.handler; + +import learningFlow.learningFlow_BE.apiPayload.code.BaseErrorCode; +import learningFlow.learningFlow_BE.apiPayload.exception.GeneralException; + +public class CollectionHandler extends GeneralException { + public CollectionHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/LoginHandler.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/LoginHandler.java new file mode 100644 index 00000000..07384d61 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/LoginHandler.java @@ -0,0 +1,10 @@ +package learningFlow.learningFlow_BE.apiPayload.exception.handler; + +import learningFlow.learningFlow_BE.apiPayload.code.BaseErrorCode; +import learningFlow.learningFlow_BE.apiPayload.exception.GeneralException; + +public class LoginHandler extends GeneralException { + public LoginHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/PageHandler.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/PageHandler.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/PageHandler.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/PageHandler.java diff --git a/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/ResourceHandler.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/ResourceHandler.java new file mode 100644 index 00000000..2e4dc24d --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/ResourceHandler.java @@ -0,0 +1,8 @@ +package learningFlow.learningFlow_BE.apiPayload.exception.handler; + +import learningFlow.learningFlow_BE.apiPayload.code.BaseErrorCode; +import learningFlow.learningFlow_BE.apiPayload.exception.GeneralException; + +public class ResourceHandler extends GeneralException { + public ResourceHandler(BaseErrorCode code) { super(code);} +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/UserHandler.java b/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/UserHandler.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/UserHandler.java rename to src/main/java/learningFlow/learningFlow_BE/apiPayload/exception/handler/UserHandler.java diff --git a/src/main/java/learningFlow/learningFlow_BE/config/AmazonConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/AmazonConfig.java new file mode 100644 index 00000000..ce08ad96 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/config/AmazonConfig.java @@ -0,0 +1,70 @@ +package learningFlow.learningFlow_BE.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.LambdaClient; + +import java.time.Duration; + +@Configuration +@Getter +public class AmazonConfig { + + private AWSCredentials awsCredentials; + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + + @PostConstruct + public void init() { + this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + @Bean + public AWSCredentialsProvider awsCredentialsProvider() { + return new AWSStaticCredentialsProvider(awsCredentials); + } + + @Bean + public LambdaClient lambdaClient() { + return LambdaClient.builder() + .region(Region.AP_NORTHEAST_2) // Lambda가 배포된 리전 + .credentialsProvider(DefaultCredentialsProvider.create()) // AWS 기본 자격 증명 사용 + .build(); + } + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java new file mode 100644 index 00000000..080ca919 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/config/CorsConfig.java @@ -0,0 +1,45 @@ +/* +package learningFlow.learningFlow_BE.config; + +//Spring Security까지 CORS 적용 목적 +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowedOrigins(Arrays.asList( + "http://localhost:3000", + "http://localhost:8081", + "http://onboarding.p-e.kr:8080", + "http://54.180.118.227", + "https://accounts.google.com" + )); + + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(Arrays.asList("*")); + config.setAllowCredentials(true); + config.setExposedHeaders(Arrays.asList( + "Authorization", + "Refresh-Token", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials" + )); + config.setMaxAge(86400L); + + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} +*/ diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/QueryDslConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/QueryDslConfig.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/config/QueryDslConfig.java rename to src/main/java/learningFlow/learningFlow_BE/config/QueryDslConfig.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/SwaggerConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/SwaggerConfig.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/config/SwaggerConfig.java rename to src/main/java/learningFlow/learningFlow_BE/config/SwaggerConfig.java diff --git a/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java new file mode 100644 index 00000000..b5040d3d --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/config/WebConfig.java @@ -0,0 +1,29 @@ +/* +package learningFlow.learningFlow_BE.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") // 개발 중일 때만 사용 + // 또는 특정 출처만 허용 + .allowedOrigins( + "http://localhost:3000", + "http://localhost:8081", + "http://onboarding.p-e.kr:8080", + "http://54.180.118.227", + "https://accounts.google.com" + ) + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(86400L); + } +} +*/ diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/security/JwtConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/security/JwtConfig.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/config/security/JwtConfig.java rename to src/main/java/learningFlow/learningFlow_BE/config/security/JwtConfig.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java b/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java similarity index 55% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java rename to src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java index 09fdecc0..142a8c2d 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java +++ b/src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java @@ -18,6 +18,12 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; @Slf4j @Configuration @@ -28,10 +34,10 @@ public class SecurityConfig { private final OAuth2UserAuthenticationService OAuth2UserAuthenticationService; private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; private final CustomAuthenticationEntryPoint authenticationEntryPoint; - @Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -43,16 +49,27 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte "/swagger-resources/**", "/webjars/**", "/find/**", - "/reset-password" + "/reset-password", + "/search/**", + "/", + "/collections/{collectionId:[\\d]+}", + "/image/upload", //이미지 업로드는 허용 + "/favicon.ico", + "/register", + "/register/complete", + "/login", + "/login/google", + "/oauth2/**", + "/login/oauth2/**", + "/user/change-email" ).permitAll() - .requestMatchers( - "/register","/register/complete", "/login", "/login/google", "/oauth2/**", "/logout/**", - "/home/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") - .requestMatchers("/user/**").authenticated() + .requestMatchers("/user/**", "/resources/**", "/collections/{collectionId:[\\d]+}/likes", "/logout/**").authenticated() .anyRequest().permitAll() ) .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable // 기본 로그아웃 처리 비활성화 + ) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(OAuth2UserAuthenticationService)) @@ -61,7 +78,6 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint)); - return http.build(); } @@ -74,4 +90,47 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Arrays.asList( + "http://localhost:3000", + "http://localhost:8081", + "http://onboarding.p-e.kr:8080", + "http://54.180.118.227", + "https://accounts.google.com", + "https://onboarding-kappa.vercel.app/", + "https://onboarding.p-e.kr" + )); + + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList( + "Authorization", + "Refresh-Token", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Content-Type", + "Accept", + "Origin", + "X-Requested-With" + )); + configuration.setAllowCredentials(true); + configuration.setExposedHeaders(Arrays.asList( + "Authorization", + "Refresh-Token", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Content-Type", + "Accept", + "Origin", + "X-Requested-With" + )); + configuration.setMaxAge(86400L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } \ No newline at end of file diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java new file mode 100644 index 00000000..d8e05c15 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/converter/CollectionConverter.java @@ -0,0 +1,159 @@ +package learningFlow.learningFlow_BE.converter; + +import learningFlow.learningFlow_BE.domain.*; +import learningFlow.learningFlow_BE.domain.enums.InterestField; +import learningFlow.learningFlow_BE.domain.enums.ResourceType; +import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; +import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CollectionConverter { + + public static List convertToHomeCollection( + List collections + ) { + return collections.stream() + .map(collection -> CollectionConverter.toCollectionPreviewDTO( + collection, + CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("BEFORE") + .progressRate(null) + .resourceDTOList(new ArrayList<>()) + .build(), + null)) + .toList(); + } + + public static SearchRequestDTO.SearchConditionDTO toSearchConditionDTO( + String keyword, + InterestField interestFields, + Integer preferMediaType, + List difficulties, + List amounts, + Integer sortType + ) { + return SearchRequestDTO.SearchConditionDTO.builder() + .keyword(keyword) + .interestFields(interestFields) + .preferMediaType(preferMediaType) + .difficulties(difficulties) + .amounts(amounts) + .sortType(sortType) + .build(); + } + + public static CollectionResponseDTO.SearchResultDTO toSearchResultDTO( + List collections, + boolean hasNext, + int totalPages, + int currentPage, + User currentUser, + Map learningInfoMap, + int totalCount + ) { + List list = collections.stream() + .map(collection -> toCollectionPreviewDTO( + collection, + learningInfoMap.get(collection.getId()), + currentUser)) + .toList(); + + return CollectionResponseDTO.SearchResultDTO.builder() + .searchResults(list) + .hasNext(hasNext) + .currentPage(currentPage) + .totalPages(totalPages) + .totalCount(totalCount) + .build(); + } + + public static CollectionResponseDTO.CollectionPreviewDTO toCollectionPreviewDTO( + Collection collection, + CollectionResponseDTO.CollectionLearningInfo learningInfo, + User currentUser + ) { + + Integer textCount = countResourcesByType(collection,ResourceType.TEXT); + + Integer videoCount = countResourcesByType(collection, ResourceType.VIDEO); + + List resourceSourceTypes = collection.getEpisodes().stream() + .map(episode -> ResourceConverter.extractResourceSource(episode.getResource().getUrl())) + .distinct() + .toList(); + + return CollectionResponseDTO.CollectionPreviewDTO.builder() + .collectionId(collection.getId()) + .imageUrl(collection.getCollectionImgUrl()) + .interestField(collection.getInterestField()) + .title(collection.getTitle()) + .creator(collection.getCreator()) + .keywords(collection.getKeywords()) + .difficulties(collection.getDifficulty()) + .amount(collection.getAmount()) + .runtime(getTotalHours(collection)) + .textCount(textCount) + .videoCount(videoCount) + .resourceSourceTypes(resourceSourceTypes) + .resource(learningInfo.getResourceDTOList()) + .likesCount(collection.getBookmarkCount()) //북마크 -> 좋아요로 이름만 변경 + .isLiked(currentUser != null && currentUser.hasBookmarked(collection.getId())) //북마크 -> 좋아요로 이름만 변경 + .progressRatePercentage(learningInfo.getProgressRate()) + .progressRatio(calculateProgressRatio(collection, learningInfo)) + .learningStatus(learningInfo.getLearningStatus()) + .startDate(learningInfo.getStartDate()) + .completedDate(learningInfo.getCompletedDate()) + .build(); + } + + private static String calculateProgressRatio(Collection collection, CollectionResponseDTO.CollectionLearningInfo learningInfo) { + if (learningInfo.getProgressRate() == null) { + return null; + } + + int totalEpisodes = collection.getEpisodes().size(); + int currentEpisode = "COMPLETED".equals(learningInfo.getLearningStatus()) + ? totalEpisodes + : learningInfo.getCurrentEpisode(); + + double progressPercentage = (double) currentEpisode / totalEpisodes * 100; + + return String.format("%d / %d회차 (%.0f%%)", + currentEpisode, + totalEpisodes, + progressPercentage); + } + + public static CollectionResponseDTO.CompletedCollectionDTO convertToCompletedCollectionDTO( + UserCollection userCollection + ) { + return CollectionResponseDTO.CompletedCollectionDTO.builder() + .collectionId(userCollection.getCollection().getId()) + .interestField(userCollection.getCollection().getInterestField()) + .collectionTitle(userCollection.getCollection().getTitle()) + .creator(userCollection.getCollection().getCreator()) + .keywords(userCollection.getCollection().getKeywords()) + .difficulties(userCollection.getCollection().getDifficulty()) + .runtime(getTotalHours(userCollection.getCollection())) + .lastAccessedTime(userCollection.getCompletedTime()) + .build(); + } + + private static int getTotalHours(Collection collection) { + int totalSeconds = collection.getEpisodes().stream() + .map(CollectionEpisode::getResource) + .mapToInt(Resource::getStudyDuration).sum(); + + return (int) Math.ceil((double) totalSeconds / 3600); + } + + private static int countResourcesByType(Collection collection, ResourceType type) { + return (int) collection.getEpisodes().stream() + .map(CollectionEpisode::getResource) + .filter(resource -> resource.getType() == type) + .count(); + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/HomeConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/HomeConverter.java new file mode 100644 index 00000000..3f00420a --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/converter/HomeConverter.java @@ -0,0 +1,43 @@ +package learningFlow.learningFlow_BE.converter; + +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.domain.User; +import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; +import learningFlow.learningFlow_BE.web.dto.home.HomeResponseDTO; + +import java.util.List; +import java.util.Map; + +public class HomeConverter { + + public static HomeResponseDTO.GuestHomeInfoDTO convertToGuestHomeInfoDTO( + List collectionPreviewList + ) { + return HomeResponseDTO.GuestHomeInfoDTO.builder() + .recommendedCollections(collectionPreviewList) + .build(); + } + + public static HomeResponseDTO.UserHomeInfoDTO convertToUserHomeInfoDTO( + CollectionResponseDTO.CollectionPreviewDTO recentLearning, + List recommendedCollections, + User user, + int size, + Map learningInfoMap + ) { + List recommendedPreviewDTOs = recommendedCollections.stream() + .distinct() + .map(collection -> CollectionConverter.toCollectionPreviewDTO( + collection, + learningInfoMap.get(collection.getId()), + user + )) + .limit(size) + .toList(); + + return HomeResponseDTO.UserHomeInfoDTO.builder() + .recentLearning(recentLearning) + .recommendedCollections(recommendedPreviewDTOs) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/MemoConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/MemoConverter.java new file mode 100644 index 00000000..03c45b59 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/converter/MemoConverter.java @@ -0,0 +1,12 @@ +package learningFlow.learningFlow_BE.converter; + +import learningFlow.learningFlow_BE.web.dto.memo.MemoRequestDTO; +import learningFlow.learningFlow_BE.web.dto.memo.MemoResponseDTO; + +public class MemoConverter { + public static MemoResponseDTO.MemoInfoDTO createMemo(MemoRequestDTO.MemoJoinDTO request){ + return MemoResponseDTO.MemoInfoDTO.builder() + .memoContents(request.getContents()) + .build(); + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java new file mode 100644 index 00000000..c2f2bc27 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/converter/ResourceConverter.java @@ -0,0 +1,181 @@ +package learningFlow.learningFlow_BE.converter; + +import learningFlow.learningFlow_BE.domain.*; +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.web.dto.resource.ResourceRequestDTO; +import learningFlow.learningFlow_BE.web.dto.resource.ResourceResponseDTO; +import learningFlow.learningFlow_BE.domain.CollectionEpisode; +import learningFlow.learningFlow_BE.domain.UserCollection; +import java.util.*; + +public class ResourceConverter { + public static ResourceResponseDTO.ResourceUrlDTO watchEpisode(Collection collection, UserEpisodeProgress userProgress, Resource resource, Optional memo){ + String memoContents = "작성하신 글의 첫 줄은 노트의 제목이 됩니다, 최대 2,000자까지 입력하실 수 있어요"; + if (memo.isPresent()) + memoContents = memo.get().getContents(); + return ResourceResponseDTO.ResourceUrlDTO.builder() + .collectionTitle(collection.getTitle()) + .interestField(collection.getInterestField()) + .resourceType(userProgress.getResourceType()) + .episodeContents(resource.getClientUrl()) + .urlTitle(resource.getTitle()) + .progress(userProgress.getCurrentProgress()) + .memoContents(memoContents) + .episodeInformationList(episodeInformationList(collection,userProgress)) + .build(); + } + public static ResourceResponseDTO.ResourceBlogUrlDTO watchBlogEpisode( + Collection collection, + UserEpisodeProgress userProgress, + String pageResource, + String resourceTitle, + Optional memo){ + String memoContents = "작성하신 글의 첫 줄은 노트의 제목이 됩니다, 최대 2,000자까지 입력하실 수 있어요"; + if (memo.isPresent()) + memoContents = memo.get().getContents(); + return ResourceResponseDTO.ResourceBlogUrlDTO.builder() + .collectionTitle(collection.getTitle()) + .interestField(collection.getInterestField()) + .resourceType(userProgress.getResourceType()) + .episodeContents(pageResource) + .urlTitle(resourceTitle) + .progress(userProgress.getCurrentProgress()) + .memoContents(memoContents) + .episodeInformationList(episodeInformationList(collection, userProgress)) + .build(); + } + + public static List episodeInformationList( + Collection collection, UserEpisodeProgress userEpisodeProgress + ) { + List episodeInformationList = new ArrayList<>(); + + for (CollectionEpisode episode : collection.getEpisodes()) { + episodeInformationList.add(new ResourceResponseDTO.episodeInformation( + episode.getId(), + episode.getEpisodeNumber(), + episode.getResource().getTitle(), + userEpisodeProgress.getIsComplete(), + episode.getResource().getType() + )); + } + episodeInformationList.sort(Comparator.comparingInt(ResourceResponseDTO.episodeInformation::getEpisodeNumber)); + return episodeInformationList; + } + + public static ResourceResponseDTO.ProgressResponseDTO toSaveProgressResponse(ResourceRequestDTO.ProgressRequestDTO request) { + return ResourceResponseDTO.ProgressResponseDTO.builder() + .progress(request.getProgress()) + .resourceType(request.getResourceType()) + .build(); + } + + public static ResourceResponseDTO.changeEpisodeIsCompleteDTO toChangeEpisodeIsCompleteDTO(Boolean isComplete){ + return ResourceResponseDTO.changeEpisodeIsCompleteDTO.builder() + .isComplete(isComplete) + .build(); + } + + public static ResourceResponseDTO.SearchResultResourceDTO convertToResourceDTO( + CollectionEpisode episode, + UserEpisodeProgress progress + ) { + return ResourceResponseDTO.SearchResultResourceDTO.builder() + .episodeId(episode.getId()) + .episodeName(episode.getEpisodeName()) + .url(episode.getResource().getUrl()) + .resourceSource(extractResourceSource(episode.getResource().getUrl())) + .episodeNumber(episode.getEpisodeNumber()) + .progress(progress != null ? progress.getCurrentProgress() : null) + .build(); + } + + public static ResourceResponseDTO.SearchResultResourceDTO convertToResourceDTO( + CollectionEpisode episode + ) { + return convertToResourceDTO(episode, null); + } + + public static ResourceResponseDTO.RecentlyWatchedEpisodeDTO convertToRecentlyWatchedEpisodeDTO( + UserCollection userCollection, + UserEpisodeProgress userEpisodeProgress + ) { + return ResourceResponseDTO.RecentlyWatchedEpisodeDTO.builder() + .resourceId(getResourceId(userCollection)) + .collectionId(userCollection.getCollection().getId()) + .collectionTitle(userCollection.getCollection().getTitle()) + .resourceSource(extractResourceSource(getResourceUrl(userCollection))) + .episodeNumber(userCollection.getUserCollectionStatus()) + .episodeName(getEpisodeName(userCollection)) + .progressRatio(calculateProgressRatio(userCollection)) + .currentProgress(userEpisodeProgress.getCurrentProgress()) + .totalProgress(userEpisodeProgress.getTotalProgress()) + .build(); + } + + public static List convertToResourceDTOWithToday( + List episodes, + int nextEpisodeNumber, + int lastCompletedEpisode + ) { + return episodes.stream() + .map(episode -> ResourceResponseDTO.SearchResultResourceDTO.builder() + .episodeId(episode.getId()) + .episodeName(episode.getEpisodeName()) + .url(episode.getResource().getUrl()) + .resourceSource(extractResourceSource(episode.getResource().getUrl())) + .episodeNumber(episode.getEpisodeNumber()) + .today(episode.getEpisodeNumber().equals(nextEpisodeNumber)) + .completed(episode.getEpisodeNumber() <= lastCompletedEpisode) + .build()) + .toList(); + } + + public static String extractResourceSource(String url) { + + String lowerCaseUrl = url.toLowerCase(); + + if (lowerCaseUrl.contains("youtube")) { + return "youtube"; + } else if (lowerCaseUrl.contains("velog")) { + return "velog"; + } else if (lowerCaseUrl.contains("naver")) { + return "naverBlog"; + } else if (lowerCaseUrl.contains("tistory")) { + return "tistory"; + } else { + return "tistory"; + } + } + + private static String getResourceUrl(UserCollection userCollection) { + return userCollection.getCollection().getEpisodes().stream() + .filter(episode -> episode.getEpisodeNumber().equals(userCollection.getUserCollectionStatus())) + .findFirst() + .map(episode -> episode.getResource().getUrl()) + .orElse(null); + } + + private static Long getResourceId(UserCollection userCollection) { + return userCollection.getCollection().getEpisodes().stream() + .filter(episode -> episode.getEpisodeNumber().equals(userCollection.getUserCollectionStatus())) + .findFirst() + .map(episode -> episode.getResource().getId()) + .orElse(null); + } + + private static String getEpisodeName(UserCollection userCollection) { + return userCollection.getCollection().getEpisodes().stream() + .filter(episode -> episode.getEpisodeNumber().equals(userCollection.getUserCollectionStatus())) + .findFirst() + .map(CollectionEpisode::getEpisodeName) + .orElse(null); + } + + private static String calculateProgressRatio(UserCollection userCollection) { + int currentEpisode = userCollection.getUserCollectionStatus(); + int totalEpisodes = userCollection.getCollection().getEpisodes().size(); + double progressPercentage = ((double) currentEpisode / totalEpisodes) * 100; + return String.format("%d / %d회차 (%.0f%%)", currentEpisode, totalEpisodes, progressPercentage); + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java b/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java new file mode 100644 index 00000000..5420ff52 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/converter/UserConverter.java @@ -0,0 +1,61 @@ +package learningFlow.learningFlow_BE.converter; + +import learningFlow.learningFlow_BE.domain.User; +import learningFlow.learningFlow_BE.domain.UserCollection; +import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; +import learningFlow.learningFlow_BE.web.dto.resource.ResourceResponseDTO; +import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; +import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO.UserInfoDTO; + +import java.util.List; + +public class UserConverter { + + public static UserResponseDTO.UserLoginResponseDTO toUserLoginResponseDTO(User user) { + + return UserResponseDTO.UserLoginResponseDTO.builder() + .loginId(user.getLoginId()) + .email(user.getEmail()) + .name(user.getName()) + .role(user.getRole()) + .socialType(user.getSocialType()) + .profileImgUrl(user.getProfileImgUrl()) + .build(); + } + + public static UserInfoDTO convertToUserInfoDTO(User user) { + return UserInfoDTO.builder() + .name(user.getName()) + .email(user.getEmail()) + .job(user.getJob()) + .interestFields(user.getInterestFields()) + .preferType(user.getPreferType()) + .profileImgUrl(user.getProfileImgUrl()) + .bannerImgUrl(user.getBannerImgUrl()) + .build(); + } + + public static UserResponseDTO.UserMyPageResponseDTO convertToUserMyPageResponseDTO( + User user, + List recentlyWatchedEpisodeDTOList, + List completedCollectionList + ) { + + return UserResponseDTO.UserMyPageResponseDTO.builder() + .userPreviewDTO(convertToUserPreviewDTO(user)) + .recentlyWatchedEpisodeList(recentlyWatchedEpisodeDTOList) + .completedCollectionList(completedCollectionList) + .build(); + } + + private static UserResponseDTO.UserPreviewDTO convertToUserPreviewDTO(User user) { + + return UserResponseDTO.UserPreviewDTO.builder() + .name(user.getName()) + .email(user.getEmail()) + .job(user.getJob().getDescription()) + .profileImgUrl(user.getProfileImgUrl()) + .bannerImgUrl(user.getBannerImgUrl()) + .build(); + } +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java b/src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java similarity index 95% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java rename to src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java index a47ffb40..782cad7f 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/BaseEntity.java @@ -10,6 +10,7 @@ import java.time.LocalDateTime; +@Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @@ -20,4 +21,5 @@ public abstract class BaseEntity { @LastModifiedDate @Column(nullable = false) private LocalDateTime updatedAt; + // 다시 바꿀것 } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Collection.java b/src/main/java/learningFlow/learningFlow_BE/domain/Collection.java similarity index 70% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Collection.java rename to src/main/java/learningFlow/learningFlow_BE/domain/Collection.java index 019b67f3..933459c7 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Collection.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/Collection.java @@ -2,8 +2,9 @@ import jakarta.persistence.*; import learningFlow.learningFlow_BE.domain.enums.InterestField; -import learningFlow.learningFlow_BE.domain.enums.MediaType; import lombok.*; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; import java.util.ArrayList; @@ -12,6 +13,8 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@DynamicInsert +@DynamicUpdate @Builder @Entity @Table(name = "collection") @@ -27,7 +30,6 @@ public class Collection extends BaseEntity { @Column(nullable = false) private String creator; - @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "collection_keywords", joinColumns = @JoinColumn(name = "collection_id")) @Column(name = "keyword") @@ -40,7 +42,6 @@ public class Collection extends BaseEntity { @Column(name = "detail_information", nullable = false) private String detailInformation; - @ElementCollection(fetch = FetchType.LAZY) @CollectionTable(name = "collection_difficulties", joinColumns = @JoinColumn(name = "collection_id")) @Column(name = "difficulty") @@ -49,13 +50,29 @@ public class Collection extends BaseEntity { @Column(nullable = false) private Integer amount; - @Enumerated(EnumType.STRING) @Column(nullable = false) - private MediaType mediaType; + private Integer resourceTypeRatio; //영상 기준 -> 100개 중 영상이 70개면 70으로 저장 -> 따라서 최댓값이 100이어야함. - @ManyToOne - @JoinColumn(name = "image_id") - private Image image; + @Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") + private Integer bookmarkCount = 0; + + @Column(nullable = false) + private String collectionImgUrl; + +// @ManyToOne +// @JoinColumn(name = "image_id") +// private Image image; +// public void setImage(Image image) { +// // 기존 이미지와의 관계 제거 +// if (this.image != null) { +// this.image.getCollections().remove(this); +// } +// this.image = image; +// // 새로운 이미지와 관계 설정 +// if (image != null) { +// image.getCollections().add(this); +// } +// } @OneToMany(mappedBy = "collection", cascade = CascadeType.ALL) private List userCollections; @@ -63,18 +80,15 @@ public class Collection extends BaseEntity { @OneToMany(mappedBy = "collection", cascade = CascadeType.ALL) private List episodes; - public void setImage(Image image) { - // 기존 이미지와의 관계 제거 - if (this.image != null) { - this.image.getCollections().remove(this); - } - this.image = image; - // 새로운 이미지와 관계 설정 - if (image != null) { - image.getCollections().add(this); - } + public void incrementBookmarkCount() { + this.bookmarkCount++; } + public void decrementBookmarkCount() { + this.bookmarkCount--; + } + + public void addUserCollection(UserCollection userCollection) { this.userCollections.add(userCollection); if (userCollection.getCollection() != this) { diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/CollectionEpisode.java b/src/main/java/learningFlow/learningFlow_BE/domain/CollectionEpisode.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/CollectionEpisode.java rename to src/main/java/learningFlow/learningFlow_BE/domain/CollectionEpisode.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java b/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java similarity index 81% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java rename to src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java index 3ec808fc..3f357989 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java @@ -1,9 +1,6 @@ package learningFlow.learningFlow_BE.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; @@ -26,6 +23,10 @@ public class EmailVerificationToken extends BaseEntity { @Column(name = "password", nullable = false) private String password; + @OneToOne(fetch = FetchType.LAZY) // optional = true 제거 + @JoinColumn(name = "user_id", nullable = true) + private User user; + @Column(name = "expiry_date", nullable = false) private LocalDateTime expiryDate; diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/Image.java b/src/main/java/learningFlow/learningFlow_BE/domain/Image.java new file mode 100644 index 00000000..2153f5b0 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/Image.java @@ -0,0 +1,37 @@ +package learningFlow.learningFlow_BE.domain; + + +//import jakarta.persistence.*; +//import lombok.*; +// +//import java.math.BigInteger; +//import java.util.List; +// +//@Getter +//@NoArgsConstructor +//@AllArgsConstructor +//@Builder +//@Entity +//@Table(name = "image") +//public class Image { +// +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private Long id; +// +// @Column(nullable = false) +// private String imageURL; +// +// @Column(nullable = false) +// private String fileType; +// +// @Column(nullable = false) +// private BigInteger fileSize; +// +// @OneToMany(mappedBy = "image", cascade = CascadeType.ALL) +// private List users; +// +// @OneToMany(mappedBy = "image", cascade = CascadeType.ALL) +// private List collections; +// +//} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Memo.java b/src/main/java/learningFlow/learningFlow_BE/domain/Memo.java similarity index 87% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Memo.java rename to src/main/java/learningFlow/learningFlow_BE/domain/Memo.java index 5602801a..765627ce 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Memo.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/Memo.java @@ -12,6 +12,6 @@ public class Memo extends BaseEntity { @EmbeddedId private MemoId id; - @Column + @Column(columnDefinition = "TEXT") private String contents; } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java b/src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java similarity index 78% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java rename to src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java index 80bf93ab..efbc9b3a 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/MemoId.java @@ -2,10 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import java.io.Serializable; @@ -13,6 +10,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode // equals와 hashCode 메소드 자동생성 public class MemoId implements Serializable { @Column(name = "collection_episode_id") private Long collectionEpisodeId; diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/PasswordResetToken.java b/src/main/java/learningFlow/learningFlow_BE/domain/PasswordResetToken.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/PasswordResetToken.java rename to src/main/java/learningFlow/learningFlow_BE/domain/PasswordResetToken.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Resource.java b/src/main/java/learningFlow/learningFlow_BE/domain/Resource.java similarity index 68% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Resource.java rename to src/main/java/learningFlow/learningFlow_BE/domain/Resource.java index 445c8c10..7842486b 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/Resource.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/Resource.java @@ -28,17 +28,20 @@ public class Resource extends BaseEntity { @Column(name = "type", nullable = false) private ResourceType type; - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "TEXT") private String url; // 자료 링크 - @Column(name = "embedded_url",nullable = false) - private String embeddedUrl; // 임베드 url + @Column(name = "client_url", columnDefinition = "TEXT") + private String clientUrl; // 클라이언트에게 보내줄 url + + @Column(nullable = false, columnDefinition = "INTEGER DEFAULT 0") + private Integer studyDuration; // 학습 시간 (초 단위 저장) @OneToMany(mappedBy = "resource", cascade = CascadeType.ALL) private List episodes; @Column(nullable = false) - private Integer runtime; //초단위로 저장(유튜브 동영상이 초단위이기 때문) + private Integer resourceQuantity; //초단위로 저장(유튜브 동영상이 초단위이기 때문) public void addEpisode(CollectionEpisode episode) { this.episodes.add(episode); @@ -53,4 +56,9 @@ public void removeEpisode(CollectionEpisode episode) { episode.setResource(null); } } + + public void setClientUrl(String clientUrl) { + if (this.clientUrl != null) return; + this.clientUrl = clientUrl; + } } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/User.java b/src/main/java/learningFlow/learningFlow_BE/domain/User.java similarity index 62% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/User.java rename to src/main/java/learningFlow/learningFlow_BE/domain/User.java index 916f7ba8..1835c891 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/User.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/User.java @@ -6,7 +6,7 @@ import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; -import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; @Getter @@ -43,19 +43,12 @@ public class User extends BaseEntity { @Column(nullable = false) private Job job; - @ElementCollection(fetch = FetchType.LAZY) + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_interests") @Enumerated(EnumType.STRING) @Column(name = "interest_field", nullable = false) private List interestFields; - @Column(name = "birth_day", nullable = false) // 년도 포함 ???년 ??월 ??일 - private LocalDate birthDay; // TODO: 생년월일 자체를 삭제할 것 - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Gender gender; - @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; @@ -67,25 +60,52 @@ public class User extends BaseEntity { @Column(name = "prefer_type", nullable = false) private MediaType preferType; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "image_id") - private Image image; + @Column(nullable = true) + private String profileImgUrl; + + @Column(nullable = true) + private String bannerImgUrl; +// @ManyToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "image_id") +// private Image image; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "user_bookmarks", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "collection_id") + private List bookmarkedCollectionIds = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List userCollections; - public void setImage(Image image) { - //기존 이미지와의 연관관계 제거 - if (this.image != null) { - this.image.getUsers().remove(this); - } - this.image = image; - //새로운 이미지와 연관관계 설정 - if (image != null) { - image.getUsers().add(this); + // added + public void addBookmark(Long collectionId) { + if (!bookmarkedCollectionIds.contains(collectionId)) { + bookmarkedCollectionIds.add(collectionId); } } + // added + public void removeBookmark(Long collectionId) { + bookmarkedCollectionIds.remove(collectionId); + } + + // added + public boolean hasBookmarked(Long collectionId) { + return bookmarkedCollectionIds.contains(collectionId); + } + +// public void setImage(Image image) { +// //기존 이미지와의 연관관계 제거 +// if (this.image != null) { +// this.image.getUsers().remove(this); +// } +// this.image = image; +// //새로운 이미지와 연관관계 설정 +// if (image != null) { +// image.getUsers().add(this); +// } +// } + public void addUserCollection(UserCollection userCollection) { this.userCollections.add(userCollection); if (userCollection.getUser() != this) { @@ -104,6 +124,10 @@ public void changePassword(String newEncodedPassword) { this.pw = newEncodedPassword; } + public void changeEmail(String newEmail) { + this.email = newEmail; + } + public void updateName(String name) { this.name = name; } @@ -116,14 +140,14 @@ public void updateInterestFields(List interestFields) { this.interestFields = interestFields; } + public void updateImage(String profileImgUrl) { + this.profileImgUrl = profileImgUrl; + } + // public void updateBirthDay(LocalDate birthDay) { // this.birthDay = birthDay; // } - public void updateGender(Gender gender) { - this.gender = gender; - } - public void updatePreferType(MediaType preferType) { this.preferType = preferType; } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java b/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java similarity index 55% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java rename to src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java index 3ea0b30c..ff1f9b0f 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/UserCollection.java @@ -1,17 +1,22 @@ package learningFlow.learningFlow_BE.domain; import jakarta.persistence.*; +import learningFlow.learningFlow_BE.domain.enums.UserCollectionStatus; import lombok.*; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; import java.time.LocalDate; @Getter @NoArgsConstructor @AllArgsConstructor +@DynamicInsert +@DynamicUpdate @Builder @Entity -@Table(name = "user_collection") -public class UserCollection { +@Table(name = "user_collection", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "collection_id"})) +public class UserCollection extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,10 +31,14 @@ public class UserCollection { private User user; @Column(name = "user_collection_status", nullable = false) - private Integer userCollectionStatus; + private Integer userCollectionStatus; // 가장 최신에 수강한 강의 저장 - @Column(name = "last_accessed_at", nullable = false) - private LocalDate lastAccessedAt; + @Column(name = "completed_time") + private LocalDate completedTime; + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "VARCHAR(20) DEFAULT 'IN_PROGRESS'", nullable = false) + private UserCollectionStatus status; public void setUser(User user) { // 기존 유저와의 관계 제거 @@ -54,5 +63,17 @@ public void setCollection(Collection collection) { collection.getUserCollections().add(this); } } + + public void setUserCollection(User user, Collection collection, Integer userCollectionStatus) { + this.user = user; + this.collection = collection; + this.userCollectionStatus = userCollectionStatus; + this.completedTime = LocalDate.now(); + } + + public void updateUserCollection(Integer userCollectionStatus) { + this.userCollectionStatus = userCollectionStatus; + this.completedTime = LocalDate.now(); + } } diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java new file mode 100644 index 00000000..0767b2be --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgress.java @@ -0,0 +1,39 @@ +package learningFlow.learningFlow_BE.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import learningFlow.learningFlow_BE.domain.enums.ResourceType; +import lombok.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "user_episode_progress") +public class UserEpisodeProgress extends BaseEntity { + + @EmbeddedId + private UserEpisodeProgressId userEpisodeProgressId; + + @Column(nullable = false) + private int episodeNumber; + + @Setter + private Integer currentProgress; + + private Integer totalProgress; + + @Column(nullable = false) + private Boolean isComplete = false; + + @Column(nullable = false) + private ResourceType resourceType; + + public Boolean setIsComplete(Boolean isComplete){ + this.isComplete = isComplete; + return this.isComplete; + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgressId.java b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgressId.java new file mode 100644 index 00000000..7318fe6c --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/UserEpisodeProgressId.java @@ -0,0 +1,21 @@ +package learningFlow.learningFlow_BE.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Embeddable +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserEpisodeProgressId implements Serializable { + @Column(name = "collection_episode_id") + private Long collectionEpisodeId; + + @Column(name = "user_id") + private String userId; +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/InterestField.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/InterestField.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/InterestField.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/InterestField.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Job.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/Job.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Job.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/Job.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/MediaType.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/MediaType.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/MediaType.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/MediaType.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java similarity index 75% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java index 77b2808f..555fbf48 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/enums/ResourceType.java @@ -1,5 +1,8 @@ package learningFlow.learningFlow_BE.domain.enums; +import lombok.Getter; + +@Getter public enum ResourceType { VIDEO, TEXT } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Role.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/Role.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/Role.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/Role.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java similarity index 75% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java rename to src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java index 8c20e574..a037123c 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java +++ b/src/main/java/learningFlow/learningFlow_BE/domain/enums/SocialType.java @@ -1,5 +1,8 @@ package learningFlow.learningFlow_BE.domain.enums; +import lombok.Getter; + +@Getter public enum SocialType { GOOGLE, LOCAL } diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/enums/UserCollectionStatus.java b/src/main/java/learningFlow/learningFlow_BE/domain/enums/UserCollectionStatus.java new file mode 100644 index 00000000..0142094e --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/enums/UserCollectionStatus.java @@ -0,0 +1,6 @@ +package learningFlow.learningFlow_BE.domain.enums; + +public enum UserCollectionStatus { + IN_PROGRESS, // 수강 중 + COMPLETED // 수강 완료 +} diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/uuid/Uuid.java b/src/main/java/learningFlow/learningFlow_BE/domain/uuid/Uuid.java new file mode 100644 index 00000000..58a8a098 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/uuid/Uuid.java @@ -0,0 +1,20 @@ +package learningFlow.learningFlow_BE.domain.uuid; + +import jakarta.persistence.*; +import learningFlow.learningFlow_BE.domain.BaseEntity; +import lombok.*; + +@Entity +@Builder +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Uuid extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String uuid; +} diff --git a/src/main/java/learningFlow/learningFlow_BE/domain/uuid/UuidRepository.java b/src/main/java/learningFlow/learningFlow_BE/domain/uuid/UuidRepository.java new file mode 100644 index 00000000..83ed8ae1 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/domain/uuid/UuidRepository.java @@ -0,0 +1,6 @@ +package learningFlow.learningFlow_BE.domain.uuid; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UuidRepository extends JpaRepository { +} diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/CollectionEpisodeRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/CollectionEpisodeRepository.java new file mode 100644 index 00000000..742e97e1 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/CollectionEpisodeRepository.java @@ -0,0 +1,9 @@ +package learningFlow.learningFlow_BE.repository; + +import learningFlow.learningFlow_BE.domain.CollectionEpisode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CollectionEpisodeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/EmailVerificationTokenRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/EmailVerificationTokenRepository.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/EmailVerificationTokenRepository.java rename to src/main/java/learningFlow/learningFlow_BE/repository/EmailVerificationTokenRepository.java diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java new file mode 100644 index 00000000..54e354aa --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/ImageRepository.java @@ -0,0 +1,7 @@ +//package learningFlow.learningFlow_BE.repository; +// +//import learningFlow.learningFlow_BE.domain.Image; +//import org.springframework.data.jpa.repository.JpaRepository; +// +//public interface ImageRepository extends JpaRepository { +//} diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/MemoRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/MemoRepository.java new file mode 100644 index 00000000..010aecf6 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/MemoRepository.java @@ -0,0 +1,19 @@ +package learningFlow.learningFlow_BE.repository; + +import learningFlow.learningFlow_BE.domain.Memo; +import learningFlow.learningFlow_BE.domain.MemoId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface MemoRepository extends JpaRepository { + @Query("SELECT m FROM Memo m WHERE m.id.collectionEpisodeId = :episodeId") + Optional findByEpisodeId(@Param("episodeId") Long episodeId); + + @Modifying + @Query("DELETE FROM Memo m WHERE m.id.userId = :loginId") + void deleteAllByUserId(@Param("loginId") String loginId); +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/PasswordResetTokenRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/PasswordResetTokenRepository.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/PasswordResetTokenRepository.java rename to src/main/java/learningFlow/learningFlow_BE/repository/PasswordResetTokenRepository.java diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/ResourceRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/ResourceRepository.java new file mode 100644 index 00000000..bf5b98ef --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/ResourceRepository.java @@ -0,0 +1,7 @@ +package learningFlow.learningFlow_BE.repository; + +import learningFlow.learningFlow_BE.domain.Resource; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ResourceRepository extends JpaRepository { +} diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/UserCollectionRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/UserCollectionRepository.java new file mode 100644 index 00000000..367ea49b --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/UserCollectionRepository.java @@ -0,0 +1,23 @@ +package learningFlow.learningFlow_BE.repository; + +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.domain.User; +import learningFlow.learningFlow_BE.domain.UserCollection; +import learningFlow.learningFlow_BE.domain.enums.UserCollectionStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface UserCollectionRepository extends JpaRepository { + Optional findByUserAndCollection(User user, Collection collection); + List findByUserAndStatusOrderByCompletedTimeDesc(User user, UserCollectionStatus status); + Optional findFirstByUserAndStatusOrderByUpdatedAtDesc(User user, UserCollectionStatus status); + + @Modifying + @Query("DELETE FROM UserCollection uc WHERE uc.user.loginId = :loginId") + void deleteAllByUserId(@Param("loginId") String loginId); +} \ No newline at end of file diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/UserEpisodeProgressRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/UserEpisodeProgressRepository.java new file mode 100644 index 00000000..ea9be24e --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/UserEpisodeProgressRepository.java @@ -0,0 +1,14 @@ +package learningFlow.learningFlow_BE.repository; + +import learningFlow.learningFlow_BE.domain.UserEpisodeProgress; +import learningFlow.learningFlow_BE.domain.UserEpisodeProgressId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserEpisodeProgressRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM UserEpisodeProgress uep WHERE uep.id.userId = :loginId") + void deleteAllByUserId(@Param("loginId") String loginId); +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/UserRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/UserRepository.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/UserRepository.java rename to src/main/java/learningFlow/learningFlow_BE/repository/UserRepository.java diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepository.java b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepository.java new file mode 100644 index 00000000..b79fa577 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepository.java @@ -0,0 +1,27 @@ +package learningFlow.learningFlow_BE.repository.collection; + +import learningFlow.learningFlow_BE.domain.Collection; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface CollectionRepository extends JpaRepository, CollectionRepositoryCustom { + List findByIdIn(List ids); + + //N+1 문제 개선을 위한 패치 조인 쿼리 + @Query("SELECT DISTINCT c FROM Collection c " + + "LEFT JOIN FETCH c.episodes e " + + "LEFT JOIN FETCH e.resource " + + "WHERE c.id = :collectionId") + Optional findByIdWithEpisodesAndResources(@Param("collectionId") Long collectionId); + + @Query("SELECT DISTINCT c FROM Collection c " + + "LEFT JOIN FETCH c.episodes e " + + "LEFT JOIN FETCH e.resource " + + "WHERE c.id IN :collectionIds") + List findByIdInWithEpisodesAndResources(@Param("collectionIds") List collectionIds); + +} diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java new file mode 100644 index 00000000..a505e232 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryCustom.java @@ -0,0 +1,22 @@ +package learningFlow.learningFlow_BE.repository.collection; + +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.domain.enums.MediaType; +import learningFlow.learningFlow_BE.domain.enums.InterestField; +import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface CollectionRepositoryCustom { + List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Pageable pageable); + Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition); + Integer getCountGreaterThanBookmark(Integer bookmarkCount, Long lastId, SearchRequestDTO.SearchConditionDTO condition); + List findTopBookmarkedCollections(int limit); + List findByInterestFieldAndPreferType(List interestFields, + MediaType preferType, + boolean matchInterest, + boolean matchPreferType, + int limit); + List findBookmarkedCollections(List bookmarkedIds, Integer sortType, Pageable pageable); +} diff --git a/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java new file mode 100644 index 00000000..c0392a20 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/repository/collection/CollectionRepositoryImpl.java @@ -0,0 +1,259 @@ +package learningFlow.learningFlow_BE.repository.collection; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; +import learningFlow.learningFlow_BE.apiPayload.exception.handler.CollectionHandler; +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.domain.QCollection; +import learningFlow.learningFlow_BE.domain.QCollectionEpisode; +import learningFlow.learningFlow_BE.domain.enums.InterestField; +import learningFlow.learningFlow_BE.domain.enums.MediaType; +import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class CollectionRepositoryImpl implements CollectionRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + private final QCollectionEpisode episode = QCollectionEpisode.collectionEpisode; + private final QCollection collection = QCollection.collection; + + @Override + public List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Pageable pageable) { + + int skip = pageable.getPageNumber() * pageable.getPageSize(); + + BooleanExpression searchConditions = createSearchConditions(condition); + +/* + if (lastId == 0L) { + return jpaQueryFactory + .select(episode.collection) + .from(episode) + .where(searchConditions) + .groupBy(episode.collection.id) + .orderBy(createOrderSpecifier(condition.getSortType())) + .limit(pageable.getPageSize()) + .fetch(); + } + + Collection lastCollection = jpaQueryFactory + .selectFrom(collection) + .where(collection.id.eq(lastId)) + .fetchOne(); + + if (lastCollection == null) { + throw new CollectionHandler(ErrorStatus.COLLECTION_NOT_FOUND); + } + + return searchNextPage(condition, lastCollection, pageable); +*/ + return jpaQueryFactory + .select(episode.collection) + .from(episode) + .where(searchConditions) + .groupBy(episode.collection.id) + .orderBy(createOrderSpecifier(condition.getSortType())) + .offset(skip) // ✅ added: 시작 위치 설정 + .limit(pageable.getPageSize()) + .fetch(); + } + + private OrderSpecifier[] createOrderSpecifier(Integer sortType) { // 반환 타입을 배열로 변경 + if (sortType == null || sortType == 0) { + return new OrderSpecifier[]{ episode.collection.id.desc() }; + } + return new OrderSpecifier[]{ + episode.collection.bookmarkCount.desc(), + episode.collection.id.desc() + }; + } + + private BooleanExpression createSearchConditions(SearchRequestDTO.SearchConditionDTO condition) { + return Expressions.allOf( + createDynamicKeyword(condition.getKeyword()), + createDynamicInterestFields(condition.getInterestFields()), + createDynamicPreferMediaType(condition.getPreferMediaType()), + createDynamicDifficulty(condition.getDifficulties()), + createDynamicAmount(condition.getAmounts()) + ); + } + + @Override + public Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition) { + + Long count = jpaQueryFactory + .select(episode.collection.countDistinct()) + .from(episode) + .where(createSearchConditions(condition)) + .fetchOne(); + return count != null ? count.intValue() : 0; + } + + private BooleanExpression createCursorCondition(Integer sortType, Collection lastCollection) { + if (sortType == null || sortType == 0) { + return episode.collection.id.lt(lastCollection.getId()); + } + + return episode.collection.bookmarkCount.lt(lastCollection.getBookmarkCount()) + .or(episode.collection.bookmarkCount.eq(lastCollection.getBookmarkCount()) + .and(episode.collection.id.lt(lastCollection.getId()))); + } + + @Override + public Integer getCountGreaterThanBookmark(Integer bookmarkCount, Long lastId, SearchRequestDTO.SearchConditionDTO condition) { + + BooleanExpression cursorCondition = createCountCursorCondition(condition.getSortType(), bookmarkCount, lastId); + + Long count = jpaQueryFactory + .select(episode.collection.countDistinct()) + .from(episode) + .where(createSearchConditions(condition), cursorCondition) + .fetchOne(); + + return count != null ? count.intValue() : 0; + } + + private BooleanExpression createCountCursorCondition(Integer sortType, Integer bookmarkCount, Long lastId) { + if (sortType == null || sortType == 0) { + return episode.collection.id.gt(lastId); + } + + return episode.collection.bookmarkCount.gt(bookmarkCount) + .or(episode.collection.bookmarkCount.eq(bookmarkCount) + .and(episode.collection.id.gt(lastId))); + } + + @Override + public List findTopBookmarkedCollections(int limit) { + return jpaQueryFactory + .selectFrom(collection) + .orderBy(collection.bookmarkCount.desc()) + .limit(limit) + .fetch(); + } + + @Override + public List findByInterestFieldAndPreferType( + List interestFields, + MediaType preferType, + boolean matchInterest, + boolean matchPreferType, + int limit + ) { + + BooleanExpression interestCondition = matchInterest ? + collection.interestField.in(interestFields) : collection.interestField.notIn(interestFields); + + BooleanExpression preferTypeCondition = null; + if (preferType != MediaType.NO_PREFERENCE) { + preferTypeCondition = preferType == MediaType.VIDEO ? + (matchPreferType ? collection.resourceTypeRatio.goe(50) : collection.resourceTypeRatio.lt(50)) : + (matchPreferType ? collection.resourceTypeRatio.lt(50) : collection.resourceTypeRatio.goe(50)); + } + + return jpaQueryFactory + .selectFrom(collection) + .where(interestCondition, preferTypeCondition) + .orderBy(collection.bookmarkCount.desc()) + .limit(limit) + .fetch(); + } + + @Override + public List findBookmarkedCollections(List bookmarkedIds, Integer sortType, Pageable pageable) { + int skip = pageable.getPageNumber() * pageable.getPageSize(); + + return jpaQueryFactory + .selectFrom(collection) + .where(collection.id.in(bookmarkedIds)) + .orderBy(createBookmarkOrderSpecifier(sortType)) + .offset(skip) + .limit(pageable.getPageSize()) + .fetch(); + } + + private OrderSpecifier[] createBookmarkOrderSpecifier(Integer sortType) { + if (sortType == null || sortType == 0) { + return new OrderSpecifier[]{ collection.createdAt.desc() }; + } + return new OrderSpecifier[]{ + collection.bookmarkCount.desc(), + collection.id.desc() + }; + } + + private BooleanExpression createDynamicInterestFields(InterestField interestFields) { + if (interestFields == null) { + return null; + } + + return episode.collection.interestField.eq(interestFields); + } + + private BooleanExpression createDynamicKeyword(String keyword) { + if (!StringUtils.hasText(keyword)) { + return null; + } + + return episode.collection.title.containsIgnoreCase(keyword) + .or(episode.collection.creator.containsIgnoreCase(keyword)) + .or(episode.collection.keywords.any().containsIgnoreCase(keyword)) + .or(episode.episodeName.containsIgnoreCase(keyword)); + } + + private BooleanExpression createDynamicPreferMediaType(Integer preferMediaType) { + + if (preferMediaType == null) { + return null; + } + + return switch (preferMediaType) { + case 1 -> episode.collection.resourceTypeRatio.eq(0); + case 2 -> episode.collection.resourceTypeRatio.loe(50); + case 4 -> episode.collection.resourceTypeRatio.goe(50); + case 5 -> episode.collection.resourceTypeRatio.eq(100); + default -> null; + }; + } + + private BooleanExpression createDynamicDifficulty(List difficulties) { + if (difficulties == null) { + return null; + } + + return episode.collection.difficulty.any().in(difficulties); + } + + private BooleanExpression createDynamicAmount(List amounts) { + if (amounts == null || amounts.isEmpty()) { + return null; + } + + BooleanExpression resultExpression = null; + + for (String amount : amounts) { + BooleanExpression expression = switch (amount) { + case "SHORT" -> episode.collection.amount.between(1, 5); + case "MEDIUM" -> episode.collection.amount.between(6, 10); + case "LONG" -> episode.collection.amount.goe(11); + default -> null; + }; + + if (expression != null) { + resultExpression = (resultExpression == null) ? expression : resultExpression.or(expression); + } + } + + return resultExpression; + } +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java b/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java similarity index 71% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java rename to src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java index 2ed972b8..9c229943 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java +++ b/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryCustom.java @@ -8,4 +8,6 @@ public interface SearchRepositoryCustom { List searchCollections(SearchRequestDTO.SearchConditionDTO condition, Long lastId, Pageable pageable); + Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition); + Integer getCountGreaterThanId(Long lastId, SearchRequestDTO.SearchConditionDTO condition); } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java b/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java similarity index 53% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java rename to src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java index d87b90b0..60a85a52 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java +++ b/src/main/java/learningFlow/learningFlow_BE/repository/search/SearchRepositoryImpl.java @@ -1,13 +1,12 @@ package learningFlow.learningFlow_BE.repository.search; -import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; import learningFlow.learningFlow_BE.apiPayload.exception.handler.PageHandler; import learningFlow.learningFlow_BE.domain.Collection; import learningFlow.learningFlow_BE.domain.QCollectionEpisode; -import learningFlow.learningFlow_BE.domain.enums.MediaType; +import learningFlow.learningFlow_BE.domain.enums.InterestField; import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -32,20 +31,66 @@ public List searchCollections(SearchRequestDTO.SearchConditionDTO co BooleanExpression cursorExp = createCursorExp(lastId); BooleanExpression keywordExp = createDynamicKeyword(condition.getKeyword()); - BooleanExpression mediaTypeExp = createDynamicMediaType(condition.getMediaType()); + BooleanExpression interestFieldExp = createDynamicInterestFields(condition.getInterestFields()); + BooleanExpression preferMediaTypeExp = createDynamicPreferMediaType(condition.getPreferMediaType()); BooleanExpression difficultyExp = createDynamicDifficulty(condition.getDifficulties()); BooleanExpression amountExp = createDynamicAmount(condition.getAmounts()); return jpaQueryFactory .select(episode.collection) .from(episode) - .where(cursorExp, keywordExp, mediaTypeExp, difficultyExp, amountExp) + .where(cursorExp, keywordExp, interestFieldExp, preferMediaTypeExp, difficultyExp, amountExp) .groupBy(episode.collection.id) .orderBy(episode.collection.id.desc()) .limit(pageable.getPageSize()) .fetch(); } + @Override + public Integer getTotalCount(SearchRequestDTO.SearchConditionDTO condition) { + BooleanExpression keywordExp = createDynamicKeyword(condition.getKeyword()); + BooleanExpression interestFieldExp = createDynamicInterestFields(condition.getInterestFields()); + BooleanExpression preferMediaTypeExp = createDynamicPreferMediaType(condition.getPreferMediaType()); + BooleanExpression difficultyExp = createDynamicDifficulty(condition.getDifficulties()); + BooleanExpression amountExp = createDynamicAmount(condition.getAmounts()); + + Long count = jpaQueryFactory + .select(episode.collection.countDistinct()) + .from(episode) + .where(keywordExp, interestFieldExp, preferMediaTypeExp, difficultyExp, amountExp) + .fetchOne(); + + return count != null ? count.intValue() : 0; + } + + @Override + public Integer getCountGreaterThanId(Long lastId, SearchRequestDTO.SearchConditionDTO condition) { + BooleanExpression keywordExp = createDynamicKeyword(condition.getKeyword()); + BooleanExpression interestFieldExp = createDynamicInterestFields(condition.getInterestFields()); + BooleanExpression preferMediaTypeExp = createDynamicPreferMediaType(condition.getPreferMediaType()); + BooleanExpression difficultyExp = createDynamicDifficulty(condition.getDifficulties()); + BooleanExpression amountExp = createDynamicAmount(condition.getAmounts()); + + // lastId보다 큰 ID를 가진 컬렉션만 카운트 + BooleanExpression greaterThanExp = lastId == 0L ? null : episode.collection.id.gt(lastId); + + Long count = jpaQueryFactory + .select(episode.collection.countDistinct()) + .from(episode) + .where(greaterThanExp, keywordExp, interestFieldExp, preferMediaTypeExp, difficultyExp, amountExp) + .fetchOne(); + + return count != null ? count.intValue() : 0; + } + + private BooleanExpression createDynamicInterestFields(InterestField interestFields) { + if (interestFields == null) { + return null; + } + + return episode.collection.interestField.eq(interestFields); + } + private BooleanExpression createCursorExp(Long lastId) { if (lastId == 0L) return null; return episode.collection.id.lt(lastId); @@ -62,12 +107,19 @@ private BooleanExpression createDynamicKeyword(String keyword) { .or(episode.episodeName.containsIgnoreCase(keyword)); } - private BooleanExpression createDynamicMediaType(MediaType mediaType) { - if (mediaType == null || mediaType == MediaType.NO_PREFERENCE) { + private BooleanExpression createDynamicPreferMediaType(Integer preferMediaType) { + + if (preferMediaType == null) { return null; } - return episode.collection.mediaType.eq(mediaType); + return switch (preferMediaType) { + case 1 -> episode.collection.resourceTypeRatio.eq(0); + case 2 -> episode.collection.resourceTypeRatio.loe(50); + case 4 -> episode.collection.resourceTypeRatio.goe(50); + case 5 -> episode.collection.resourceTypeRatio.eq(100); + default -> null; + }; } private BooleanExpression createDynamicDifficulty(List difficulties) { diff --git a/src/main/java/learningFlow/learningFlow_BE/s3/AmazonS3Manager.java b/src/main/java/learningFlow/learningFlow_BE/s3/AmazonS3Manager.java new file mode 100644 index 00000000..54266946 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/s3/AmazonS3Manager.java @@ -0,0 +1,78 @@ +package learningFlow.learningFlow_BE.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; + +import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; +import learningFlow.learningFlow_BE.apiPayload.exception.GeneralException; +import learningFlow.learningFlow_BE.config.AmazonConfig; +import learningFlow.learningFlow_BE.domain.uuid.Uuid; +import learningFlow.learningFlow_BE.domain.uuid.UuidRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AmazonS3Manager { + + private final AmazonS3 amazonS3; + + private final AmazonConfig amazonConfig; + + private final UuidRepository uuidRepository; + + public String uploadFile(String keyName, MultipartFile file){ + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); // Content-Type 설정 + metadata.setContentDisposition("inline"); // Content-Disposition 설정 + + try { + amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), keyName, file.getInputStream(), metadata)); + + } catch (IOException e){ + log.error("error at AmazonS3Manager uploadFile : {}", (Object) e.getStackTrace()); + throw new GeneralException(ErrorStatus.IMAGE_UPLOAD_FAILED); + } + + return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString(); + } + + public String generateKeyName(Uuid uuid) { + return uuid.getUuid(); + } + + public String uploadImageToS3(MultipartFile imageFile) { + try { + // UUID 생성 및 저장 + String imageUuid = UUID.randomUUID().toString(); + Uuid savedUuid = uuidRepository.save(Uuid.builder() + .uuid(imageUuid).build()); + + // 이미지 업로드 + String imageKey = generateKeyName(savedUuid); // KeyName 생성 + String imageUrl = uploadFile(imageKey, imageFile); // 업로드된 URL 반환 + + // 업로드 성공 여부 확인 + if (imageUrl == null || imageUrl.isEmpty()) { + throw new GeneralException(ErrorStatus._BAD_REQUEST); // 업로드 실패 시 예외 처리 + } + + return imageUrl; // 성공 시 URL 반환 + + } catch (GeneralException e) { + log.error("이미지 업로드 실패: {}", e.getMessage()); + throw e; // GeneralException은 그대로 전달 + } catch (Exception e) { + log.error("이미지 업로드 중 내부 오류 발생: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.IMAGE_UPLOAD_FAILED); // 기타 예외 처리 + } + } +} diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/auth/PrincipalDetails.java b/src/main/java/learningFlow/learningFlow_BE/security/auth/PrincipalDetails.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/security/auth/PrincipalDetails.java rename to src/main/java/learningFlow/learningFlow_BE/security/auth/PrincipalDetails.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/CustomAuthenticationEntryPoint.java b/src/main/java/learningFlow/learningFlow_BE/security/handler/CustomAuthenticationEntryPoint.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/CustomAuthenticationEntryPoint.java rename to src/main/java/learningFlow/learningFlow_BE/security/handler/CustomAuthenticationEntryPoint.java diff --git a/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java b/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java new file mode 100644 index 00000000..a88b7084 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/security/handler/JwtLogoutHandler.java @@ -0,0 +1,34 @@ +package learningFlow.learningFlow_BE.security.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtLogoutHandler implements LogoutHandler { + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + // SecurityContext 초기화 + SecurityContextHolder.clearContext(); + + // Authorization 헤더 제거 + response.setHeader("Authorization", ""); + response.setHeader("Refresh-Token", ""); + + response.setHeader("Access-Control-Expose-Headers", "Authorization, Refresh-Token"); + + log.info("현재 Authorization 헤더 값: {}", response.getHeader("Authorization")); + log.info("현재 Refresh-Token 헤더 값: {}", response.getHeader("Refresh-Token")); + + log.info("로그아웃 처리 완료: {}", + authentication != null ? authentication.getName() : "Unknown user"); + } +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java b/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java similarity index 56% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java rename to src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java index dc3c8ff6..914667fd 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/learningFlow/learningFlow_BE/security/handler/OAuth2LoginSuccessHandler.java @@ -1,15 +1,15 @@ package learningFlow.learningFlow_BE.security.handler; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import learningFlow.learningFlow_BE.apiPayload.ApiResponse; import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; import learningFlow.learningFlow_BE.security.jwt.JwtTokenProvider; import learningFlow.learningFlow_BE.service.auth.oauth.OAuth2UserTemp; -import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -17,8 +17,6 @@ import java.io.IOException; -import static learningFlow.learningFlow_BE.converter.UserConverter.toUserLoginResponseDTO; - @Component @RequiredArgsConstructor @Slf4j @@ -26,6 +24,9 @@ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; + @Value("${app.frontend-url}") + private String frontendUrl; + @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { @@ -34,7 +35,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // Principal 타입 확인, 첫 로그인인 경우 회원가입으로 이동 if (authentication.getPrincipal() instanceof OAuth2UserTemp oAuth2UserTemp) { String temporaryToken = jwtTokenProvider.createTemporaryToken(oAuth2UserTemp); - String redirectUrl = "/oauth2/additional-info?token=" + temporaryToken; + String redirectUrl = frontendUrl + "/oauth2/additional-info?oauth2RegistrationCode=" + temporaryToken; response.sendRedirect(redirectUrl); return; } @@ -48,16 +49,23 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo //Access Token 생성 String accessToken = jwtTokenProvider.createAccessToken(authentication); log.info("Access 토큰 발급 : {}", accessToken); - response.addHeader("Authorization", "Bearer " + accessToken); +// response.setHeader("Authorization", "Bearer " + accessToken); String refreshToken = jwtTokenProvider.createRefreshToken(authentication); log.info("자동 로그인 활성화, Refresh Token 발급 : {}", refreshToken); - response.addHeader("Refresh-Token", refreshToken); +// response.setHeader("Refresh-Token", refreshToken); // 헤더 설정 확인 로깅 log.info("Authorization Header: {}", response.getHeader("Authorization")); log.info("Refresh-Token Header: {}", response.getHeader("Refresh-Token")); +/* + response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Expose-Headers", "Authorization, Refresh-Token"); +*/ + +/* UserResponseDTO.UserLoginResponseDTO loginResponse = toUserLoginResponseDTO(principalDetails.getUser()); @@ -65,5 +73,34 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo response.setCharacterEncoding("UTF-8"); String jsonResponse = new ObjectMapper().writeValueAsString(ApiResponse.onSuccess(loginResponse)); response.getWriter().write(jsonResponse); +*/ +// response.setStatus(HttpStatus.OK.value()); + + // HTTP-Only 쿠키 설정 (Refresh Token) + ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) // HTTPS에서만 + .path("/") + .maxAge(7 * 24 * 60 * 60) + .sameSite("None") // 필요하면 Lax, Strict + .build(); + response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + + + response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Expose-Headers", "Authorization, Refresh-Token"); + + + + // ⭐️ 팝업 창을 닫고 부모 창에 메시지 전달하는 스크립트 (프론트엔드 도메인 사용) + String redirectScript = ""; + response.setContentType("text/html;charset=UTF-8"); + response.getWriter().write(redirectScript); } } diff --git a/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..589868cd --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,145 @@ +package learningFlow.learningFlow_BE.security.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import learningFlow.learningFlow_BE.apiPayload.ApiResponse; +import learningFlow.learningFlow_BE.service.user.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + log.info("🔍 [JwtAuthenticationFilter] 요청 수신: {}", request.getRequestURI()); + String jwt = getJwtFromRequest(request); + log.info("🟡 [JwtAuthenticationFilter] 추출된 JWT: {}", jwt); + + try { + // JWT 토큰이 있는 경우 검증 및 인증 처리 + if (StringUtils.hasText(jwt)) { + if (jwtTokenProvider.validateToken(jwt)) { + log.info("유효한 Access Token"); + processValidToken(request, jwt); + } else { + log.info("Access Token이 만료되어 Refresh Token 확인을 시도"); + processExpiredToken(request, response); + } + } else { + // JWT 토큰이 없는 경우, 허용된 URL이 아니면 401 에러 + if (!isPermitAllUrl(request.getRequestURI())) { + log.error("❌ [JwtAuthenticationFilter] 인증되지 않은 요청 → 401 반환"); + handleAuthenticationError(response, "로그인이 필요한 서비스입니다."); + return; + } + } + } catch (Exception e) { + log.error("❌ [JwtAuthenticationFilter] 예외 발생: {}", e.getMessage(), e); + if (!isPermitAllUrl(request.getRequestURI())) { + handleAuthenticationError(response, "로그인이 필요한 서비스입니다."); + return; + } + } + + filterChain.doFilter(request, response); + } + + private void processValidToken(HttpServletRequest request, String jwt) { + String email = jwtTokenProvider.getEmailFromToken(jwt); + log.info("토큰에서 추출한 이메일: {}", email); + + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("인증 정보 SecurityContext에 저장"); + } + + private void processExpiredToken(HttpServletRequest request, HttpServletResponse response) { + String refreshToken = request.getHeader("Refresh-Token"); + log.info("전달받은 Refresh Token: {}", refreshToken); + + if (StringUtils.hasText(refreshToken) && jwtTokenProvider.validateToken(refreshToken)) { + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + String newAccessToken = jwtTokenProvider.createAccessToken(authentication); + log.info("새로 발급된 Access Token: {}", newAccessToken); + response.addHeader("Authorization", "Bearer " + newAccessToken); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("새로운 Access Token으로 인증 정보를 업데이트 완료"); + } + } + + private void handleAuthenticationError(HttpServletResponse response, String message) throws IOException { + log.error("🚨 [JwtAuthenticationFilter] 인증 실패: {}", message); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write( + new ObjectMapper().writeValueAsString( + ApiResponse.onFailure("AUTH4001", message, null) + ) + ); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + log.info("🟡 [JwtAuthenticationFilter] Authorization 헤더: {}", bearerToken); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private boolean isPermitAllUrl(String requestURI) { + + return requestURI.equals("/") || + requestURI.equals("/login") || + requestURI.equals("/register") || + requestURI.startsWith("/register/complete") || + requestURI.equals("/login/google") || + requestURI.startsWith("/oauth2") || + requestURI.startsWith("/swagger-ui") || + requestURI.startsWith("/v3/api-docs") || + requestURI.startsWith("/swagger-resources") || + requestURI.startsWith("/webjars") || + requestURI.startsWith("/find") || + requestURI.startsWith("/search") || + requestURI.equals("/reset-password") || + requestURI.matches("/collections/\\d+") || + requestURI.contains("/user/change-email") || + requestURI.startsWith("/user/imgUpload"); // 이미지 업로드는 인증 없이 허용 + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + boolean shouldSkip = path.equals("/image/upload") || path.equals("/favicon.ico"); + log.info("🛑 [JwtAuthenticationFilter] shouldNotFilter 실행: path={}, shouldSkip={}", path, shouldSkip); + return shouldSkip; + } +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtProperties.java b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtProperties.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtProperties.java rename to src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtProperties.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtTokenProvider.java b/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtTokenProvider.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtTokenProvider.java rename to src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtTokenProvider.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/common/AuthTokenCleanupScheduler.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/common/AuthTokenCleanupScheduler.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/common/AuthTokenCleanupScheduler.java rename to src/main/java/learningFlow/learningFlow_BE/service/auth/common/AuthTokenCleanupScheduler.java diff --git a/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java new file mode 100644 index 00000000..9e0bb04f --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java @@ -0,0 +1,268 @@ +package learningFlow.learningFlow_BE.service.auth.common; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserVerificationEmailService { + + private final JavaMailSender emailSender; + + @Value("${app.frontend-url}") + private String baseUrl; + + public void sendVerificationEmail(String email, String token) { + try { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setTo(email); + helper.setSubject("[OnBoarding] 이메일 인증"); + + String htmlContent = """ + + + + + + + + + + + + + + +
+
+
+ OnBoarding +
+ +
+

+ 회원가입을 위해 메일을 인증해주세요 +

+ +

+ 안녕하세요, OnBoarding입니다.
+ 회원가입을 완료하기 위해 메일을 인증해주세요.
+ 버튼을 누르면 자동으로 인증 후 추가 정보 입력 페이지로 이동합니다. +

+ + + 이메일 인증하기 + + +

+ 이 메일은 24시간 동안 유효합니다.
+ 본인이 요청하지 않은 경우, 이 메일을 무시해주세요. +

+
+
+
+ + + """.formatted(baseUrl, token); + + helper.setText(htmlContent, true); + emailSender.send(message); + log.info("이메일 인증 메일 발송 완료: {}", email); + } catch (MessagingException e) { + log.error("이메일 발송 실패: {}", e.getMessage()); + throw new RuntimeException("이메일 발송에 실패했습니다."); + } + } + + public void sendEmailResetEmail(String email, String token) { + try { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setTo(email); + helper.setSubject("[OnBoarding] 이메일 인증"); + + String htmlContent = """ + + + + + + + + + + + + + + +
+
+
+ OnBoarding +
+ +
+

+ 이메일을 변경하기 위해 메일을 인증해주세요 +

+ +

+ 안녕하세요, OnBoarding입니다.
+ 이메일 변경을 완료하기 위해 메일을 인증해주세요.
+ 버튼을 누르면 자동으로 인증 후 이메일 변경이 완료됩니다. +

+ + + 이메일 인증하기 + + +

+ 이 메일은 24시간 동안 유효합니다.
+ 본인이 요청하지 않은 경우, 이 메일을 무시해주세요. +

+
+
+
+ + + """.formatted(baseUrl, token); + + helper.setText(htmlContent, true); + emailSender.send(message); + log.info("이메일 인증 메일 발송 완료: {}", email); + } catch (MessagingException e) { + log.error("이메일 발송 실패: {}", e.getMessage()); + throw new RuntimeException("이메일 발송에 실패했습니다."); + } + } + + public void sendPasswordResetEmail(String email, String passwordResetCode) { + + try { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setTo(email); + helper.setSubject("[OnBoarding] 비밀번호 재설정"); + + String htmlContent = """ + + + + + + + + + + + + + + +
+
+
+ OnBoarding +
+ +
+

+ 비밀번호 재설정을 위해 메일을 인증해주세요 +

+ +

+ 안녕하세요, OnBoarding입니다.
+ 비밀번호 재설정을 위해 메일을 인증해주세요
+ 버튼을 누르면 자동으로 인증 후 비밀번호 재설정 페이지로 이동합니다. +

+ + + 이메일 인증하기 + + +

+ 이 메일은 24시간 동안 유효합니다.
+ 본인이 요청하지 않은 경우, 이 메일을 무시해주세요. +

+
+
+
+ + + """.formatted(baseUrl, passwordResetCode); + + helper.setText(htmlContent, true); + emailSender.send(message); + log.info("이메일 인증 메일 발송 완료: {}", email); + } catch (MessagingException e) { + log.error("이메일 발송 실패: {}", e.getMessage()); + throw new RuntimeException("이메일 발송에 실패했습니다."); + } + } +} \ No newline at end of file diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java similarity index 51% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java rename to src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java index afc3cdd8..3b5948e8 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java +++ b/src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java @@ -1,6 +1,13 @@ package learningFlow.learningFlow_BE.service.auth.local; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; +import learningFlow.learningFlow_BE.apiPayload.exception.GeneralException; +import learningFlow.learningFlow_BE.domain.uuid.Uuid; +import learningFlow.learningFlow_BE.domain.uuid.UuidRepository; +import learningFlow.learningFlow_BE.s3.AmazonS3Manager; +import learningFlow.learningFlow_BE.apiPayload.exception.handler.LoginHandler; import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; import learningFlow.learningFlow_BE.security.handler.JwtLogoutHandler; import learningFlow.learningFlow_BE.security.jwt.JwtTokenProvider; @@ -25,6 +32,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; import java.util.Optional; @@ -45,18 +53,21 @@ public class LocalUserAuthService { private final EmailVerificationTokenRepository emailVerificationTokenRepository; private final JwtTokenProvider jwtTokenProvider; private final JwtLogoutHandler jwtLogoutHandler; + private final AmazonS3Manager s3Manager; + private final UuidRepository uuidRepository; @Transactional public void initialRegister(UserRequestDTO.InitialRegisterDTO requestDTO) { // 이메일 중복 체크 if (userRepository.existsByEmail(requestDTO.getEmail())) { - throw new RuntimeException("이미 사용중인 이메일입니다."); + throw new GeneralException(ErrorStatus.EMAIL_ALREADY_EXISTS); } // 진행 중인 이메일 인증이 있는지 확인 if (emailVerificationTokenRepository.existsByEmailAndVerifiedFalse(requestDTO.getEmail())) { - throw new RuntimeException("이미 진행 중인 이메일 인증이 있습니다. 이메일을 확인해주세요."); + throw new GeneralException(ErrorStatus.EMAIL_VERIFICATION_IN_PROGRESS); } + //TODO: 현재는 진행중이던 이메일이면 500에러가 나는데 400에러가 나야함! -> 확인 바랍니다! // 토큰 생성 String token = UUID.randomUUID().toString(); @@ -77,14 +88,14 @@ public void initialRegister(UserRequestDTO.InitialRegisterDTO requestDTO) { } @Transactional - public EmailVerificationToken validateRegistrationToken(String token) { + public EmailVerificationToken validateRegistrationToken(String emailVerificationCode) { // 토큰 유효성 검증 - EmailVerificationToken verificationToken = emailVerificationTokenRepository.findByTokenAndVerifiedFalse(token) - .orElseThrow(() -> new RuntimeException("유효하지 않은 토큰입니다.")); + EmailVerificationToken verificationToken = emailVerificationTokenRepository.findByTokenAndVerifiedFalse(emailVerificationCode) + .orElseThrow(() -> new GeneralException(ErrorStatus.EMAIL_CODE_INVALID)); if (verificationToken.isExpired()) { emailVerificationTokenRepository.delete(verificationToken); - throw new RuntimeException("만료된 토큰입니다. 회원가입을 다시 진행해주세요."); + throw new GeneralException(ErrorStatus.EMAIL_CODE_EXPIRED); } return verificationToken; @@ -92,16 +103,26 @@ public EmailVerificationToken validateRegistrationToken(String token) { @Transactional public UserResponseDTO.UserLoginResponseDTO completeRegister( - String token, + String emailVerificationCode, UserRequestDTO.CompleteRegisterDTO requestDTO, HttpServletResponse response ) { +// String profileImgUrl = null; //이메일 토큰 검증 - EmailVerificationToken verificationToken = validateRegistrationToken(token); + EmailVerificationToken verificationToken = validateRegistrationToken(emailVerificationCode); // 로그인 ID 생성 - String uuid = UUID.randomUUID().toString().substring(0, 8); - String loginId = "LOCAL_" + uuid; + String loginuuid = UUID.randomUUID().toString().substring(0, 8); + String loginId = "LOCAL_" + loginuuid; + +// //이미지 파일 +// if (imageFile != null && !imageFile.isEmpty()) { +// log.info("이미지 업로드 요청 발생"); +// imageUrl = s3Manager.uploadImageToS3(imageFile); +// // user 엔티티에 이미지 URL 업데이트 +// //user.updateImage(imageUrl); +// } + // 새로운 유저 생성 User user = User.builder() @@ -111,11 +132,10 @@ public UserResponseDTO.UserLoginResponseDTO completeRegister( .name(requestDTO.getName()) .job(requestDTO.getJob()) .interestFields(requestDTO.getInterestFields()) - .birthDay(requestDTO.getBirthDay()) - .gender(requestDTO.getGender()) .preferType(requestDTO.getPreferType()) .socialType(SocialType.LOCAL) .role(Role.USER) + .profileImgUrl(requestDTO.getImgProfileUrl()) .inactive(false) .build(); @@ -127,6 +147,37 @@ public UserResponseDTO.UserLoginResponseDTO completeRegister( return toUserLoginResponseDTO(savedUser); } +// private String uploadImageToS3(MultipartFile imageFile) { +// if (imageFile == null || imageFile.isEmpty()) { +// throw new GeneralException(ErrorStatus.IMAGE_FORMAT_BADREQUEST); // 이미지 파일 검증 실패 +// } +// +// try { +// // UUID 생성 및 저장 +// String imageUuid = UUID.randomUUID().toString(); +// Uuid savedUuid = uuidRepository.save(Uuid.builder() +// .uuid(imageUuid).build()); +// +// // 이미지 업로드 +// String imageKey = s3Manager.generateKeyName(savedUuid); // KeyName 생성 +// String imageUrl = s3Manager.uploadFile(imageKey, imageFile); // 업로드된 URL 반환 +// +// // 업로드 성공 여부 확인 +// if (imageUrl == null || imageUrl.isEmpty()) { +// throw new GeneralException(ErrorStatus._BAD_REQUEST); // 업로드 실패 시 예외 처리 +// } +// +// return imageUrl; // 성공 시 URL 반환 +// +// } catch (GeneralException e) { +// log.error("이미지 업로드 실패: {}", e.getMessage()); +// throw e; // GeneralException은 그대로 전달 +// } catch (Exception e) { +// log.error("이미지 업로드 중 내부 오류 발생: {}", e.getMessage()); +// throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); // 기타 예외 처리 +// } +// } + public UserResponseDTO.UserLoginResponseDTO login(UserRequestDTO.UserLoginDTO request, HttpServletResponse response) { try { @@ -165,9 +216,17 @@ public UserResponseDTO.UserLoginResponseDTO login(UserRequestDTO.UserLoginDTO re } @Transactional - public void sendPasswordResetEmail(UserRequestDTO.FindPasswordDTO request) { + public String sendPasswordResetEmail(PrincipalDetails principalDetails) { + + if (principalDetails == null || principalDetails.getUser() == null) { + throw new LoginHandler(ErrorStatus.USER_NOT_FOUND); + } + User user = principalDetails.getUser(); + +/* User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new RuntimeException("해당 이메일로 가입된 계정이 없습니다.")); +*/ if (user.getSocialType() != SocialType.LOCAL) { throw new RuntimeException("구글 로그인으로 가입된 계정입니다."); @@ -191,30 +250,129 @@ public void sendPasswordResetEmail(UserRequestDTO.FindPasswordDTO request) { tokenRepository.save(resetToken); userVerificationEmailService.sendPasswordResetEmail(user.getEmail(), token); + + return "비밀번호 재설정 링크를 담은 이메일이 성공적으로 발송되었습니다."; } @Transactional - public void resetPassword(UserRequestDTO.ResetPasswordDTO request) { - PasswordResetToken resetToken = tokenRepository.findByToken(request.getToken()) - .orElseThrow(() -> new RuntimeException("유효하지 않은 토큰입니다.")); + public PasswordResetToken validatePasswordResetToken(String passwordResetCode) { + PasswordResetToken resetToken = tokenRepository.findByToken(passwordResetCode) + .orElseThrow(() -> new GeneralException(ErrorStatus.PASSWORD_RESET_CODE_INVALID)); if (resetToken.isExpired()) { tokenRepository.delete(resetToken); - throw new RuntimeException("만료된 토큰입니다. 비밀번호 재설정을 다시 요청해주세요."); + throw new GeneralException(ErrorStatus.PASSWORD_RESET_CODE_EXPIRED); } + return resetToken; + } + + @Transactional + public String resetPassword( + String passwordResetCode, + UserRequestDTO.ResetPasswordDTO request + ) { + PasswordResetToken resetToken = validatePasswordResetToken(passwordResetCode); + User user = resetToken.getUser(); + + if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPw())) { + throw new GeneralException(ErrorStatus.PASSWORD_CURRENT_MISMATCH); + } + + if (passwordEncoder.matches(request.getNewPassword(), user.getPw())) { + throw new GeneralException(ErrorStatus.PASSWORD_SAME_AS_CURRENT); + } + user.changePassword(passwordEncoder.encode(request.getNewPassword())); // 사용된 토큰 삭제 tokenRepository.delete(resetToken); log.info("비밀번호 재설정 완료: {}", user.getEmail()); + + return "비밀번호 재설정이 완료되었습니다."; } - public void logout(String token) { - if (token != null && token.startsWith("Bearer ")) { - token = token.substring(7); + @Transactional + public String sendEmailResetEmail(String email, PrincipalDetails principalDetails) { + + //구글 로그인 유저인지 확인 + if (principalDetails.getUser().getSocialType().equals(SocialType.GOOGLE)) { + throw new GeneralException(ErrorStatus.GOOGLE_USER_CANNOT_CHANGE_EMAIL); + } + + // 이메일 중복 체크 + if (userRepository.existsByEmail(email)) { + throw new GeneralException(ErrorStatus.EMAIL_ALREADY_EXISTS); + } + + //현재 이메일과 동일한 이메일로는 변경 불가 + if (principalDetails.getUser().getEmail().equals(email)) { + throw new GeneralException(ErrorStatus.EMAIL_CHANGE_SAME_AS_CURRENT); + } + + // 진행 중인 이메일 인증이 있는지 확인 + if (emailVerificationTokenRepository.existsByEmailAndVerifiedFalse(email)) { + throw new GeneralException(ErrorStatus.EMAIL_VERIFICATION_IN_PROGRESS); + } + + // 토큰 생성 + String token = UUID.randomUUID().toString(); + + // 이메일 인증 토큰 저장 + EmailVerificationToken verificationToken = EmailVerificationToken.builder() + .token(token) + .email(email) + .password(passwordEncoder.encode(principalDetails.getPassword())) + .user(principalDetails.getUser()) + .expiryDate(LocalDateTime.now().plusHours(24)) + .verified(false) + .build(); + + emailVerificationTokenRepository.save(verificationToken); + + // 인증 이메일 발송 + userVerificationEmailService.sendEmailResetEmail(email, token); + + return "이메일 재설정을 위한 인증 링크를 담은 이메일이 성공적으로 발송되었습니다."; + } + + @Transactional + public String changeEmail( + EmailVerificationToken emailVerificationToken + ) { + User user = emailVerificationToken.getUser(); + if (user == null) { + throw new GeneralException(ErrorStatus.USER_NOT_FOUND); + } + + // 이메일 업데이트 + user.changeEmail(emailVerificationToken.getEmail()); + + // 토큰 삭제 + emailVerificationTokenRepository.delete(emailVerificationToken); + + return "이메일이 성공적으로 변경되었습니다."; + } + + @Transactional + public String logout(HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + + log.info("로그아웃 전 Authorization 헤더: {}", request.getHeader("Authorization")); + log.info("로그아웃 전 Refresh-Token 헤더: {}", request.getHeader("Refresh-Token")); + + jwtLogoutHandler.logout(request, response, authentication); + + log.info("로그아웃 후 Authorization 헤더: {}", response.getHeader("Authorization")); + log.info("로그아웃 후 Refresh-Token 헤더: {}", response.getHeader("Refresh-Token")); + log.info("로그아웃 완료: {}", authentication.getName()); + + return "로그아웃 성공"; + } else { + log.info("이미 로그아웃된 상태입니다"); + return "이미 로그아웃된 상태입니다"; } - jwtLogoutHandler.addToBlacklist(token); } } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserAuthenticationService.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserAuthenticationService.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserAuthenticationService.java rename to src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserAuthenticationService.java diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java similarity index 84% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java rename to src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java index 873dee3d..56574d7a 100644 --- a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java +++ b/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserRegistrationService.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; +import learningFlow.learningFlow_BE.s3.AmazonS3Manager; import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; import learningFlow.learningFlow_BE.security.jwt.JwtTokenProvider; import learningFlow.learningFlow_BE.domain.User; @@ -13,7 +14,6 @@ import learningFlow.learningFlow_BE.web.dto.user.UserResponseDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; import static learningFlow.learningFlow_BE.converter.UserConverter.toUserLoginResponseDTO; @@ -36,14 +35,15 @@ public class OAuth2UserRegistrationService { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; - private final RedisTemplate redisTemplate; +// private final RedisTemplate redisTemplate; + private final AmazonS3Manager s3Manager; // 추가 정보 입력 필요 여부와 필드 정보를 반환하는 메소드 public Map getAdditionalInfoRequirements() { Map response = new HashMap<>(); response.put("message", "추가 정보 입력이 필요합니다"); response.put("requiredFields", Arrays.asList( - "job", "interestFields", "birthDay", "gender", "preferType" + "name", "job", "interestFields", "preferType" )); return response; @@ -55,13 +55,14 @@ public UserResponseDTO.UserLoginResponseDTO updateAdditionalInfo( UserRequestDTO.AdditionalInfoDTO additionalInfo, HttpServletResponse response) { + String imageUrl = null; + if (!jwtTokenProvider.validateToken(temporaryToken) || !jwtTokenProvider.isTemporaryToken(temporaryToken)) { throw new RuntimeException("유효하지 않은 토큰입니다."); } Claims claims = jwtTokenProvider.getClaims(temporaryToken); String email = claims.getSubject(); - String name = claims.get("name", String.class); String providerId = claims.get("providerId", String.class); SocialType socialType = SocialType.valueOf(claims.get("socialType", String.class)); @@ -69,15 +70,14 @@ public UserResponseDTO.UserLoginResponseDTO updateAdditionalInfo( User newUser = User.builder() .loginId(socialType.name() + "_" + providerId) .email(email) - .name(name) + .name(additionalInfo.getName()) .providerId(providerId) .pw("OAUTH2_USER") .socialType(socialType) .job(additionalInfo.getJob()) .interestFields(additionalInfo.getInterestFields()) - .birthDay(additionalInfo.getBirthDay()) - .gender(additionalInfo.getGender()) .preferType(additionalInfo.getPreferType()) + .profileImgUrl(additionalInfo.getImgProfileUrl()) .role(Role.USER) .inactive(false) .build(); @@ -97,15 +97,9 @@ public UserResponseDTO.UserLoginResponseDTO updateAdditionalInfo( log.info("Access 토큰 발급 : {}", accessToken); String refreshToken = jwtTokenProvider.createRefreshToken(authentication); - response.addHeader("Refersh-Token", refreshToken); + response.addHeader("Refresh-Token", refreshToken); log.info("자동 로그인 활성화, Refresh Token 발급 : {}", refreshToken); - //임시 토큰 블랙리스트에 저장 - redisTemplate.opsForValue() - .set("BLACKLIST:" + temporaryToken, "true", - jwtTokenProvider.getRemainingTime(temporaryToken), - TimeUnit.MILLISECONDS); - return toUserLoginResponseDTO(savedUser); } } diff --git a/learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserTemp.java b/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserTemp.java similarity index 100% rename from learningFlow/src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserTemp.java rename to src/main/java/learningFlow/learningFlow_BE/service/auth/oauth/OAuth2UserTemp.java diff --git a/src/main/java/learningFlow/learningFlow_BE/service/collection/CollectionService.java b/src/main/java/learningFlow/learningFlow_BE/service/collection/CollectionService.java new file mode 100644 index 00000000..630dd00e --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/service/collection/CollectionService.java @@ -0,0 +1,348 @@ +package learningFlow.learningFlow_BE.service.collection; + +import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; +import learningFlow.learningFlow_BE.apiPayload.exception.handler.CollectionHandler; +import learningFlow.learningFlow_BE.converter.CollectionConverter; +import learningFlow.learningFlow_BE.converter.HomeConverter; +import learningFlow.learningFlow_BE.converter.ResourceConverter; +import learningFlow.learningFlow_BE.domain.*; +import learningFlow.learningFlow_BE.domain.Collection; +import learningFlow.learningFlow_BE.domain.enums.UserCollectionStatus; +import learningFlow.learningFlow_BE.repository.UserCollectionRepository; +import learningFlow.learningFlow_BE.repository.UserEpisodeProgressRepository; +import learningFlow.learningFlow_BE.repository.collection.CollectionRepository; +import learningFlow.learningFlow_BE.security.auth.PrincipalDetails; +import learningFlow.learningFlow_BE.web.dto.home.HomeResponseDTO; +import learningFlow.learningFlow_BE.web.dto.resource.ResourceResponseDTO; +import learningFlow.learningFlow_BE.web.dto.search.SearchRequestDTO; +import learningFlow.learningFlow_BE.web.dto.collection.CollectionResponseDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CollectionService { + + private final UserEpisodeProgressRepository userEpisodeProgressRepository; + private final CollectionRepository collectionRepository; + private final UserCollectionRepository userCollectionRepository; + private static final int PAGE_SIZE = 8; + private static final int HOME_COLLECTION_SIZE = 6; + + public CollectionResponseDTO.CollectionPreviewDTO CollectionDetails(Long collectionId, PrincipalDetails principalDetails) { + + Authentication authentication = (principalDetails != null) ? SecurityContextHolder.getContext().getAuthentication() : null; + + Collection collection = collectionRepository.findByIdWithEpisodesAndResources(collectionId) //N+1 문제 개선 + .orElseThrow(() -> new CollectionHandler(ErrorStatus.COLLECTION_NOT_FOUND)); + + User currentUser = null; + if (authentication != null && authentication.getPrincipal() instanceof PrincipalDetails) { + currentUser = ((PrincipalDetails) authentication.getPrincipal()).getUser(); + } + + CollectionResponseDTO.CollectionLearningInfo learningInfo = getLearningInfo(collection, currentUser, true); + + return CollectionConverter.toCollectionPreviewDTO(collection, learningInfo, currentUser); + } + + private List getAllResources( + Collection collection + ) { + List episodes = collection.getEpisodes(); + + // 모든 에피소드를 에피소드 번호 순으로 정렬 + return episodes.stream() + .sorted(Comparator.comparing(CollectionEpisode::getEpisodeNumber)) + .map(ResourceConverter::convertToResourceDTO) + .toList(); + } + + private List getAllResources( + Collection collection, + User user + ) { + List episodes = collection.getEpisodes(); + + return episodes.stream() + .sorted(Comparator.comparing(CollectionEpisode::getEpisodeNumber)) + .map(episode -> { + UserEpisodeProgress progress = null; + if (user != null) { + UserEpisodeProgressId progressId = new UserEpisodeProgressId(episode.getId(), user.getLoginId()); + progress = userEpisodeProgressRepository.findById(progressId).orElse(null); + } + return ResourceConverter.convertToResourceDTO(episode, progress); + }) + .toList(); + } + + public CollectionResponseDTO.SearchResultDTO search(SearchRequestDTO.SearchConditionDTO condition, Integer page, PrincipalDetails principalDetails) { + + Authentication authentication = (principalDetails != null) ? SecurityContextHolder.getContext().getAuthentication() : null; + + PageRequest pageRequest = PageRequest.of(page - 1, PAGE_SIZE); + List collections = collectionRepository.searchCollections(condition, pageRequest); + + if (collections.isEmpty()) { + return CollectionConverter.toSearchResultDTO(collections, false, 0, 0, null, null, 0); + } + + Collection lastCollection = collections.getLast(); + + Integer totalCount = collectionRepository.getTotalCount(condition); + + int totalPages = (int) Math.ceil((double) totalCount / PAGE_SIZE); + boolean hasNext = page < totalPages; + + int currentPage = calculateCurrentPage(condition, lastCollection); + + User currentUser; + if (authentication != null && authentication.getPrincipal() instanceof PrincipalDetails) { + currentUser = ((PrincipalDetails) authentication.getPrincipal()).getUser(); + } else { + currentUser = null; + } + + Map learningInfoMap = collections.stream() + .collect(Collectors.toMap( + Collection::getId, + collection -> getLearningInfo(collection, currentUser, false) + )); + + return CollectionConverter.toSearchResultDTO( + collections, + hasNext, + totalPages, + currentPage, + currentUser, + learningInfoMap, + totalCount + ); + } + + public CollectionResponseDTO.CollectionLearningInfo getLearningInfo( + Collection collection, + User user, + boolean isDetailView + ) { + if (user == null) { + return CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("BEFORE") + .progressRate(null) + .resourceDTOList(isDetailView ? + getAllResources(collection, null) : // 상세 조회면 전체 리소스 + getFilteredResources(collection, null, 0)) // 아니면 필터링된 리소스 + .build(); + } + + Optional userCollection = userCollectionRepository.findByUserAndCollection(user, collection); + + if (userCollection.isEmpty()) { + return CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("BEFORE") + .progressRate(null) + .resourceDTOList(isDetailView ? + getAllResources(collection, user) : // 상세 조회면 전체 리소스 + getFilteredResources(collection, null, 0)) // 아니면 필터링된 리소스 + .build(); + } + + UserCollection realUserCollection = userCollection.get(); + if (realUserCollection.getStatus() == UserCollectionStatus.COMPLETED) { + return CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("COMPLETED") + .progressRate(100) + .startDate(realUserCollection.getCreatedAt().toLocalDate()) + .completedDate(realUserCollection.getCompletedTime()) + .resourceDTOList(isDetailView ? + getAllResources(collection) : // 상세 조회면 전체 리소스 + new ArrayList<>()) // 아니면 빈 리스트 + .build(); + } + + int progressRate = calculateProgressRate(realUserCollection); + return CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("IN_PROGRESS") + .progressRate(progressRate) + .startDate(realUserCollection.getCreatedAt().toLocalDate()) + .currentEpisode(realUserCollection.getUserCollectionStatus()) + .resourceDTOList(isDetailView ? + getAllResources(collection, user) : + getFilteredResources(collection, user, realUserCollection.getUserCollectionStatus())) + .build(); + } + + private int calculateProgressRate(UserCollection userCollection) { + return (int) Math.round((double) userCollection.getUserCollectionStatus() / + userCollection.getCollection().getEpisodes().size() * 100); + } + + private List getFilteredResources( + Collection collection, User user, int currentEpisode + ) { + List episodes = collection.getEpisodes(); + List filteredEpisodes; + + //수강 완료일때는 resource에 뭐 담을 필요 없음 + if (user != null) { + Optional userCollection = userCollectionRepository.findByUserAndCollection(user, collection); + if (userCollection.isPresent() && userCollection.get().getStatus() == UserCollectionStatus.COMPLETED) { + return new ArrayList<>(); + } + } + + if (user == null || currentEpisode == 0) { + // 비회원이거나 수강 전인 경우 처음 4개 + filteredEpisodes = episodes.stream() + .sorted(Comparator.comparing(CollectionEpisode::getEpisodeNumber)) + .limit(4) + .toList(); + } else { + // 수강 중인 경우 현재 회차부터 3개 + filteredEpisodes = episodes.stream() + .sorted(Comparator.comparing(CollectionEpisode::getEpisodeNumber)) + .filter(ep -> ep.getEpisodeNumber() >= currentEpisode) + .limit(4) + .toList(); + } + + return filteredEpisodes.stream() + .map(ResourceConverter::convertToResourceDTO) + .toList(); + } + + public HomeResponseDTO.GuestHomeInfoDTO getGuestHomeCollections() { + List collections = collectionRepository.findTopBookmarkedCollections(HOME_COLLECTION_SIZE); + + List GuestCollectionList = collections.stream() + .map(collection -> { + CollectionResponseDTO.CollectionLearningInfo defaultLearningInfo = + CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("BEFORE") + .progressRate(null) + .resourceDTOList(getFilteredResources(collection, null, 0)) + .build(); + return CollectionConverter.toCollectionPreviewDTO(collection, defaultLearningInfo, null); + }) + .toList(); + + return HomeConverter.convertToGuestHomeInfoDTO(GuestCollectionList); + } + + public HomeResponseDTO.UserHomeInfoDTO getUserHomeCollections(User user) { + // 최근 학습 컬렉션 조회 + CollectionResponseDTO.CollectionPreviewDTO recentLearning = getRecentLearning(user); + + // 추천 컬렉션 목록 조회 + // 1. interestField와 preferType 모두 충족 + List recommendedCollections = new ArrayList<>(collectionRepository.findByInterestFieldAndPreferType( + user.getInterestFields(), user.getPreferType(), true, true, HOME_COLLECTION_SIZE + )); + + // 2. interestField만 충족 + if (recommendedCollections.size() < HOME_COLLECTION_SIZE) { + recommendedCollections.addAll( + collectionRepository.findByInterestFieldAndPreferType( + user.getInterestFields(), user.getPreferType(), true, false, + HOME_COLLECTION_SIZE - recommendedCollections.size() + ) + ); + } + + // 3. preferType만 충족 + if (recommendedCollections.size() < HOME_COLLECTION_SIZE) { + recommendedCollections.addAll( + collectionRepository.findByInterestFieldAndPreferType( + user.getInterestFields(), user.getPreferType(), false, true, + HOME_COLLECTION_SIZE - recommendedCollections.size() + ) + ); + } + + // 4. 모두 불충족 + if (recommendedCollections.size() < HOME_COLLECTION_SIZE) { + recommendedCollections.addAll( + collectionRepository.findByInterestFieldAndPreferType( + user.getInterestFields(), user.getPreferType(), false, false, + HOME_COLLECTION_SIZE - recommendedCollections.size() + ) + ); + } + + Map learningInfoMap = recommendedCollections.stream() + .collect(Collectors.toMap( + Collection::getId, + collection -> getLearningInfo(collection, user, false) + )); + + return HomeConverter.convertToUserHomeInfoDTO( + recentLearning, + recommendedCollections, + user, + HOME_COLLECTION_SIZE, + learningInfoMap + ); + } + + private CollectionResponseDTO.CollectionPreviewDTO getRecentLearning(User user) { + + return userCollectionRepository + .findFirstByUserAndStatusOrderByUpdatedAtDesc(user, UserCollectionStatus.IN_PROGRESS) + .map(userCollection -> { + Collection collection = userCollection.getCollection(); + + List episodes = collection.getEpisodes(); + int totalEpisodes = episodes.size(); + int currentEpisode = userCollection.getUserCollectionStatus(); + int nextEpisode = currentEpisode + 1; + + List selectedEpisodes = new ArrayList<>(); + if (totalEpisodes <= 4) { + // 4회차 이하의 컬렉션은 전체 표시 + selectedEpisodes.addAll(episodes); + } else { + // 5회차 이상의 컬렉션 + // today 에피소드(다음 에피소드)가 가능한 2번째에 오도록 계산 + int idealStart = nextEpisode - 2; // today가 2번째에 오기 위한 이상적인 시작 인덱스 + + // 실제 시작 인덱스 계산 (0 이상, totalEpisodes-4 이하) + int startIdx = Math.max(0, Math.min(idealStart, totalEpisodes - 4)); + + // 4개의 에피소드 선택 + for (int i = 0; i < 4; i++) { + selectedEpisodes.add(episodes.get(startIdx + i)); + } + } + + CollectionResponseDTO.CollectionLearningInfo learningInfo = CollectionResponseDTO.CollectionLearningInfo.builder() + .learningStatus("IN_PROGRESS") + .progressRate(calculateProgressRate(userCollection)) + .startDate(userCollection.getCreatedAt().toLocalDate()) + .currentEpisode(currentEpisode) + .resourceDTOList(ResourceConverter.convertToResourceDTOWithToday( + selectedEpisodes, + nextEpisode, + currentEpisode + )) + .build(); + + return CollectionConverter.toCollectionPreviewDTO(collection, learningInfo, user); + }) + .orElse(null); + } + + private int calculateCurrentPage(SearchRequestDTO.SearchConditionDTO condition, Collection lastCollection) { + return collectionRepository.getCountGreaterThanBookmark(lastCollection.getBookmarkCount(),lastCollection.getId(), condition) / PAGE_SIZE + 1; + } +} diff --git a/src/main/java/learningFlow/learningFlow_BE/service/embed/BlogEmbedService.java b/src/main/java/learningFlow/learningFlow_BE/service/embed/BlogEmbedService.java new file mode 100644 index 00000000..5acdbab2 --- /dev/null +++ b/src/main/java/learningFlow/learningFlow_BE/service/embed/BlogEmbedService.java @@ -0,0 +1,163 @@ +/* +package learningFlow.learningFlow_BE.service.embed; + +import learningFlow.learningFlow_BE.apiPayload.code.status.ErrorStatus; +import learningFlow.learningFlow_BE.apiPayload.exception.handler.ResourceHandler; +import learningFlow.learningFlow_BE.domain.CollectionEpisode; +import learningFlow.learningFlow_BE.domain.Resource; +import learningFlow.learningFlow_BE.domain.enums.ResourceType; +import learningFlow.learningFlow_BE.repository.CollectionEpisodeRepository; +import learningFlow.learningFlow_BE.service.lambda.LambdaService; +import lombok.RequiredArgsConstructor; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.zip.GZIPOutputStream; + +@Service +@RequiredArgsConstructor +@Transactional +public class BlogEmbedService { + private final CollectionEpisodeRepository collectionEpisodeRepository; +*/ +/* private final LambdaService lambdaService; + public String getResource(Long episodeId){ + CollectionEpisode episode = collectionEpisodeRepository.findById(episodeId) + .orElseThrow(() -> new ResourceHandler(ErrorStatus.EPISODE_NOT_FOUND)); + Resource resource = episode.getResource(); + // 블로그 임베드 url 미 생성인 경우 + if (resource.getType() == ResourceType.TEXT + && resource.getClientUrl() == null) { + return lambdaService.invokeLambda(u); + } + // 이미 생성된 경우 + return resource.getClientUrl(); + }*//* + + +*/ +/* + @Async // 비동기 처리 + public CompletableFuture getBlogSource(Long episodeId) { + + CollectionEpisode episode = collectionEpisodeRepository.findById(episodeId) + .orElseThrow(() -> new ResourceHandler(ErrorStatus.EPISODE_NOT_FOUND)); + String blogUrl = episode.getResource().getUrl(); + + // Headless 모드 설정 + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless"); // GUI 없이 실행 + options.addArguments("--no-sandbox"); // 리눅스 환경에서 필요한 옵션 + options.addArguments("--disable-dev-shm-usage"); // 메모리 문제 방지 + options.addArguments("--disable-gpu"); // GPU 가속 비활성화 (필요 시) + // 서버 과부화 문제로 이미지 + 자바스크립트 요청 X -> 나중에 지워야함 + options.addArguments("--blink-settings=imagesEnabled=false"); // ✅ 이미지 로드 방지 + options.addArguments("--disable-javascript"); // ✅ JavaScript 실행 방지 + + // 🔹 User-Agent를 일반적인 브라우저처럼 설정 + options.addArguments("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + options.addArguments("--disable-blink-features=AutomationControlled"); + + + WebDriver driver = null; + + try { + String seleniumUrl = "http://172.31.38.3:4444"; + driver = new RemoteWebDriver(new URL(seleniumUrl), options); + + // ✅ 페이지 로드 타임아웃 설정 (기본 무한대기 방지) + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(20)); + + driver.get(blogUrl); + + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20)); // ✅ Duration.ofSeconds()로 변경 + wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("body"))); + + // JavaScript 실행 후 전체 HTML 가져오기 + JavascriptExecutor js = (JavascriptExecutor) driver; + String fullHTML = (String) js.executeScript("return document.documentElement.outerHTML;"); + + // 광고 제거 + String cleanedHtml = removeAdsFromHtml(fullHTML); + + // JSON 포맷으로 변환 + String jsonResponse = "{\"html\":\"" + escapeJson(cleanedHtml) + "\"}"; + + // Gzip 압축 + byte[] gzippedResponse = compressGzip(jsonResponse); + + return CompletableFuture.completedFuture(gzippedResponse); + } + catch (TimeoutException e) { + killChromeProcesses(); + throw new RuntimeException("페이지 로드 시간이 초과되었습니다.", e); + } + catch (IOException e) { + killChromeProcesses(); + throw new RuntimeException("Gzip 압축 중 오류 발생", e); // IOException 처리 + } finally { + if (driver != null) { + try { + driver.quit(); + } catch (Exception e) { + System.out.println("WebDriver 종료 중 오류 발생: " + e.getMessage()); + } + } + } + } +*//* + +*/ +/* + // 광고 코드 제거 + private String removeAdsFromHtml(String html) { + return html.replaceAll("(?i)]*>(.*?)", "") // 모든