diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..d02df72
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,58 @@
+# This workflow will build a Swift project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
+
+name: CI
+
+on:
+ pull_request:
+ branches: [ "develop" ]
+
+jobs:
+ build:
+ runs-on: macos-15
+
+ steps:
+ - name: 1️⃣ Checkout Project
+ uses: actions/checkout@v4
+
+ - name: 2️⃣ SSH key 설치
+ uses: shimataro/ssh-key-action@v2
+ with:
+ key: ${{ secrets.SSH_KEY }}
+ known_hosts: ${{ secrets.KNOWN_HOSTS }}
+
+ - name: 3️⃣ 보안 설정 파일 가져오기
+ env:
+ CONFIG_REPO: ${{ secrets.PRIVATE_REPO }}
+ TEMP_DIR: "temp_private_configs"
+ run: |
+ git clone $CONFIG_REPO $TEMP_DIR
+
+ SOURCE_PATH="$TEMP_DIR/PrivateFile"
+
+ echo "📁 파일 복사 중: $SOURCE_PATH"
+ cp "$SOURCE_PATH/Debug.xcconfig" xcconfigs/
+ cp "$SOURCE_PATH/Release.xcconfig" xcconfigs/
+ cp "$SOURCE_PATH/.env.default" fastlane/
+
+ rm -rf "$TEMP_DIR"
+ echo "✅ 보안 파일 배치 완료"
+
+ # mise-action을 써서 tuist, ruby 등 도구 설치 (PATH 자동 등록)
+ - name: 4️⃣ mise를 통한 도구 설치 (Tuist/Ruby)
+ uses: jdx/mise-action@v2
+ with:
+ install: true
+
+ - name: 5️⃣ 의존성 설치 및 프로젝트 생성
+ run: |
+ bundle install
+ tuist install
+ tuist generate --no-open
+
+ - name: 6️⃣ Build Check & Discord Notification
+ env:
+ DISCORD_URL: ${{ secrets.DISCORD_URL }}
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+ run: |
+ bundle exec fastlane ci_check
diff --git a/.gitignore b/.gitignore
index 5183772..9fbec88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,3 +87,6 @@ Tuist/.build
# Fastlane.swift runner binary
**/fastlane/FastlaneRunner
+
+# 환경 변수
+**/fastlane/.env.default
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..be94e6f
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+3.2.2
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..1cae64d
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,49 @@
+# 검사에서 제외할 디렉토리 지정
+excluded:
+ - Tuist
+ - Tuist/Dependencies
+ - Carthage
+ - Pods
+ - .build
+ - Derived
+ - fastlane
+ - "**/*.generated.swift"
+ - "**/Derived/**"
+ - "**/*.tuist.generated.swift"
+ - "**/TuistBundle+*.swift"
+ - "**/TuistAssets+*.swift"
+ - "**/Project.swift"
+ - Scripts
+
+# 추가로 활성화할 규칙들
+opt_in_rules:
+ - empty_count # count == 0 대신 isEmpty 사용 권장
+ - empty_string # "" 대신 String() 사용 권장
+ - fatal_error_message # fatalError 메시지 필수
+ - first_where # filter().first 대신 first(where:) 사용 권장
+ - force_unwrapping # 강제 언래핑(!) 사용 제한
+ - implicit_return # 암시적 return 사용 제한
+ - modifier_order # 수정자 순서 강제
+ - operator_usage_whitespace # 연산자 주변 공백
+ - sorted_imports # import 알파벳 순 정렬
+
+# 비활성화할 일반적인 규칙
+disabled_rules:
+ - trailing_whitespace
+ - todo
+ - trailing_newline
+ - multiple_closures_with_trailing_closure
+ - leading_whitespace
+
+# 라인 길이 설정
+line_length:
+ warning: 120
+ error: 300
+
+# 타입 본문 길이 설정
+type_body_length:
+ warning: 400
+ error: 500
+
+analyzer_rules:
+ - unused_import # 사용하지 않는 import
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..f80c3f6
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+gem "fastlane", "~> 2.230"
+
+plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
+eval_gemfile(plugins_path) if File.exist?(plugins_path)
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..ec5668b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,330 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
+ artifactory (3.0.17)
+ atomos (0.1.3)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1206.0)
+ aws-sdk-core (3.241.4)
+ aws-eventstream (~> 1, >= 1.3.0)
+ aws-partitions (~> 1, >= 1.992.0)
+ aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
+ jmespath (~> 1, >= 1.6.1)
+ logger
+ aws-sdk-kms (1.121.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.212.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.12.1)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ base64 (0.2.0)
+ bigdecimal (4.0.1)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ csv (3.3.5)
+ declarative (0.0.20)
+ digest-crc (0.7.0)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.6.20240107)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.112.0)
+ faraday (1.10.4)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.8)
+ faraday (>= 0.8.0)
+ http-cookie (>= 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.1)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.2.0)
+ multipart-post (~> 2.0)
+ faraday-net_http (1.0.2)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.1)
+ faraday (~> 1.0)
+ fastimage (2.4.0)
+ fastlane (2.231.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ abbrev (~> 0.1.2)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ base64 (~> 0.2.0)
+ bundler (>= 1.17.3, < 5.0.0)
+ colored (~> 1.2)
+ commander (~> 4.6)
+ csv (~> 3.3)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.0.0)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ http-cookie (~> 1.0.5)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ logger (>= 1.6, < 2.0)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3.0)
+ naturally (~> 2.2)
+ nkf (~> 0.2.0)
+ optparse (>= 0.1.1, < 1.0.0)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.5)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (~> 3)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.4.1)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-sirp (1.0.0)
+ sysrandom (~> 1.0)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.54.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.3)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.31.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-cloud-core (1.8.0)
+ google-cloud-env (>= 1.0, < 3.a)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.5.0)
+ google-cloud-storage (1.47.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.31.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.8.1)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.8)
+ domain_name (~> 0.5)
+ httpclient (2.9.0)
+ mutex_m
+ jmespath (1.6.2)
+ json (2.18.0)
+ jwt (2.10.2)
+ base64
+ logger (1.7.0)
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.19.1)
+ multipart-post (2.4.1)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
+ naturally (2.3.0)
+ nkf (0.2.0)
+ optparse (0.8.1)
+ os (1.1.4)
+ plist (3.7.2)
+ public_suffix (7.0.2)
+ rake (13.3.1)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.4.4)
+ rouge (3.28.0)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.4.1)
+ security (0.1.5)
+ signet (0.21.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 4.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ sysrandom (1.0.5)
+ terminal-notifier (2.0.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.2)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unicode-display_width (2.6.0)
+ word_wrap (1.0.0)
+ xcodeproj (1.27.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.4.0)
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ arm64-darwin-24
+ ruby
+
+DEPENDENCIES
+ fastlane (~> 2.230)
+
+CHECKSUMS
+ CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
+ abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
+ addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
+ artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
+ atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
+ aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
+ aws-partitions (1.1206.0) sha256=9016c2d4f5d633aeb95d4cfeb936b5de3d752a5dbae4838bc19d53f202749301
+ aws-sdk-core (3.241.4) sha256=a42ccba8c24ea9800e7b6c40aa201c967458f7c460044a6eebf64fbf1226e4fd
+ aws-sdk-kms (1.121.0) sha256=d563c1cfb4b5754efbc671216c8eca875338748adad0f42518c28dfa0a2d01e0
+ aws-sdk-s3 (1.212.0) sha256=1e8ec4eee6914818dc79840e05c04fba9e61e8aa4822eef283fe593b93397c19
+ aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
+ babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
+ base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
+ bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
+ claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
+ colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
+ colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
+ commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
+ csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
+ declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
+ digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
+ domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
+ dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
+ emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
+ excon (0.112.0) sha256=daf9ac3a4c2fc9aa48383a33da77ecb44fa395111e973084d5c52f6f214ae0f0
+ faraday (1.10.4) sha256=a384c541cde688d68bf85055723aecb4100c3fa41b53beb2011b245960ab2f19
+ faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
+ faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
+ faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
+ faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
+ faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
+ faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
+ faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
+ faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
+ faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
+ faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
+ faraday-retry (1.0.3) sha256=add154f4f399243cbe070806ed41b96906942e7f5259bb1fe6daf2ec8f497194
+ faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
+ fastimage (2.4.0) sha256=5fce375e27d3bdbb46c18dbca6ba9af29d3304801ae1eb995771c4796c5ac7e8
+ fastlane (2.231.0) sha256=69523272108f1212d0c6a6e6985f22341d0828c11ebc62a7a55a08f595d7721c
+ fastlane-sirp (1.0.0) sha256=66478f25bcd039ec02ccf65625373fca29646fa73d655eb533c915f106c5e641
+ gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
+ google-apis-androidpublisher_v3 (0.54.0) sha256=8970a72839c8dfa87d290bdf935c641bf18cbd4323bf71d182adc04c0108d210
+ google-apis-core (0.11.3) sha256=43217013b129d7d52c31ebf94146646c55f463ed25e68ad7523fb644d5a9cc97
+ google-apis-iamcredentials_v1 (0.17.0) sha256=9a6525cfd6ef1c9a355f593bfef11bd0fb30e1d785ef9e5c9da51c3817a0517b
+ google-apis-playcustomapp_v1 (0.13.0) sha256=959e51f90454b51adc72e5c322b4b4a573f869520c4d7c7c20efd1d262e48fd6
+ google-apis-storage_v1 (0.31.0) sha256=03e8cc775e12403e5878899dd4bfb90957e7281e894937f03641c3857262db8f
+ google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf
+ google-cloud-env (1.6.0) sha256=6179acb946975892c7908748df5722a4ebadfc8cf5bb7b0d8d933ca67183fa15
+ google-cloud-errors (1.5.0) sha256=b56be28b8c10628125214dde571b925cfcebdbc58619e598250c37a2114f7b4b
+ google-cloud-storage (1.47.0) sha256=b543d01a9c83495149accd2da77b2cf365d5b6aac21bf3fa21e69dfc8f1eeb76
+ googleauth (1.8.1) sha256=814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7
+ highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
+ http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
+ httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
+ jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
+ json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
+ jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
+ mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
+ multi_json (1.19.1) sha256=7aefeff8f2c854bf739931a238e4aea64592845e0c0395c8a7d2eea7fdd631b7
+ multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
+ mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
+ nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
+ naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
+ nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126
+ optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
+ os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
+ plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
+ public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
+ representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
+ retriable (3.1.2) sha256=0a5a5d0ca4ba61a76fb31a17ab8f7f80281beb040c329d34dfc137a1398688e0
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
+ rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
+ ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
+ rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
+ security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
+ signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b
+ simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
+ sysrandom (1.0.5) sha256=5ac1ac3c2ec64ef76ac91018059f541b7e8f437fbda1ccddb4f2c56a9ccf1e75
+ terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
+ terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
+ trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
+ tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
+ tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
+ tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
+ uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
+ unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
+ word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
+ xcodeproj (1.27.0) sha256=8cc7a73b4505c227deab044dce118ede787041c702bc47636856a2e566f854d3
+ xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
+ xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
+
+BUNDLED WITH
+ 4.0.3
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4baa2bf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+generate:
+ tuist install
+ tuist generate
+
+clean:
+ rm -rf **/**/**/*.xcodeproj
+ rm -rf **/**/*.xcodeproj
+ rm -rf **/*.xcodeproj
+ rm -rf *.xcworkspace
+
+reset:
+ tuist clean
+ make clean
+
+regenerate:
+ make clean
+ tuist generate
+
+# Feature 모듈 생성
+feature:
+ @swift Scripts/GenerateModule.swift
+ tuist edit
+
+# 팀원이 파일을 모두 배치한 후 실행
+setup:
+ @chmod +x Scripts/Onboarding.sh
+ @/bin/bash Scripts/Onboarding.sh
+
+# 디바이스 추가
+device:
+ bundle exec fastlane register_new_device
diff --git a/Plugins/ConfigPlugin/Plugin.swift b/Plugins/ConfigPlugin/Plugin.swift
new file mode 100644
index 0000000..ff1a242
--- /dev/null
+++ b/Plugins/ConfigPlugin/Plugin.swift
@@ -0,0 +1,10 @@
+//
+// Plugin.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+let configPlugin = Plugin(name: "ConfigPlugin")
diff --git a/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift b/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift
new file mode 100644
index 0000000..68683c9
--- /dev/null
+++ b/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift
@@ -0,0 +1,35 @@
+//
+// Configurations.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public struct XCConfig {
+ public struct Path {
+ static var debug: ProjectDescription.Path {
+ .relativeToRoot("xcconfigs/Debug.xcconfig")
+ }
+
+ static var release: ProjectDescription.Path {
+ .relativeToRoot("xcconfigs/Release.xcconfig")
+ }
+ }
+
+ public static let framework: [Configuration] = [
+ .debug(name: "Debug", xcconfig: Path.debug),
+ .release(name: "Release", xcconfig: Path.release)
+ ]
+
+ public static let demo: [Configuration] = [
+ .debug(name: "Debug", xcconfig: Path.debug),
+ .release(name: "Release", xcconfig: Path.release)
+ ]
+
+ public static let prod: [Configuration] = [
+ .debug(name: "Debug", xcconfig: Path.debug),
+ .release(name: "Release", xcconfig: Path.release)
+ ]
+}
diff --git a/Plugins/DependencyPlugin/Plugin.swift b/Plugins/DependencyPlugin/Plugin.swift
new file mode 100644
index 0000000..2798840
--- /dev/null
+++ b/Plugins/DependencyPlugin/Plugin.swift
@@ -0,0 +1,10 @@
+//
+// Plugin.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+let dependencyPlugin = Plugin(name: "DependencyPlugin")
diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift
new file mode 100644
index 0000000..4e65b76
--- /dev/null
+++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift
@@ -0,0 +1,44 @@
+//
+// Dependency+Project.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public extension TargetDependency {
+ struct Features {
+ public struct Home {}
+ }
+
+ struct Modules {}
+}
+
+public extension TargetDependency {
+ static let data = TargetDependency.project(target: "Data", path: .data)
+ static let domain = TargetDependency.project(target: "Domain", path: .domain)
+ static let core = TargetDependency.project(target: "Core", path: .core)
+}
+
+public extension TargetDependency.Modules {
+ static let dsKit = TargetDependency.project(target: "DSKit", path: .relativeToModules("DSKit"))
+ static let networks = TargetDependency.project(target: "Networks", path: .relativeToModules("Networks"))
+ static let thirdPartyLibs = TargetDependency.project(target: "ThirdPartyLibs", path: .relativeToModules("ThirdPartyLibs"))
+}
+
+public extension TargetDependency.Features {
+ static func project(name: String, group: String) -> TargetDependency {
+ .project(target: "\(group)\(name)", path: .relativeToFeature("\(group)\(name)"))
+ }
+
+ static let baseFeatureDependency = TargetDependency.project(target: "BaseFeatureDependency", path: .relativeToFeature("BaseFeatureDependency"))
+
+ static let rootFeature = TargetDependency.project(target: "RootFeature", path: .relativeToFeature("RootFeature"))
+}
+
+public extension TargetDependency.Features.Home {
+ static let group = "Home"
+
+ static let feature = TargetDependency.Features.project(name: "Feature", group: group)
+}
diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift
new file mode 100644
index 0000000..d4083ed
--- /dev/null
+++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift
@@ -0,0 +1,21 @@
+//
+// Dependency+SPM.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public extension TargetDependency {
+ enum SPM {}
+}
+
+public extension TargetDependency.SPM {
+ static let SnapKit = TargetDependency.external(name: "SnapKit")
+ static let Then = TargetDependency.external(name: "Then")
+ static let Kingfisher = TargetDependency.external(name: "Kingfisher")
+ static let Moya = TargetDependency.external(name: "Moya")
+ static let RxSwift = TargetDependency.external(name: "RxSwift")
+ static let RIBs = TargetDependency.external(name: "RIBs")
+}
diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Path+.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Path+.swift
new file mode 100644
index 0000000..65c3b68
--- /dev/null
+++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Path+.swift
@@ -0,0 +1,35 @@
+//
+// Path+.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public extension ProjectDescription.Path {
+ static func relativeToFeature(_ path: String) -> Self {
+ return .relativeToRoot("Projects/Features/\(path)")
+ }
+
+ static func relativeToModules(_ path: String) -> Self {
+ return .relativeToRoot("Projects/Modules/\(path)")
+ }
+
+ static var app: Self {
+ return .relativeToRoot("Projects/App")
+ }
+
+ static var data: Self {
+ return .relativeToRoot("Projects/Data")
+ }
+
+ static var domain: Self {
+ return .relativeToRoot("Projects/Domain")
+ }
+
+ static var core: Self {
+ return .relativeToRoot("Projects/Core")
+ }
+}
+
diff --git a/Plugins/EnvPlugin/Plugin.swift b/Plugins/EnvPlugin/Plugin.swift
new file mode 100644
index 0000000..fe0d2a3
--- /dev/null
+++ b/Plugins/EnvPlugin/Plugin.swift
@@ -0,0 +1,10 @@
+//
+// Plugin.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+let envPlugin = Plugin(name: "EnvPlugin")
diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Environment.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Environment.swift
new file mode 100644
index 0000000..bd6bfb3
--- /dev/null
+++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Environment.swift
@@ -0,0 +1,29 @@
+//
+// Environment.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public enum Environment {
+ public static let workspaceName = "NDGL-iOS"
+ public static let deploymentTarget = "17.0"
+ public static let bundleId = "org.yapp.NDGL"
+
+ public struct App {
+ public static let displayName = "나도갈래"
+ public static let version = "1.0.0"
+ public static let buildNumber = "1"
+ }
+}
+
+public extension Project {
+ enum Environment {
+ public static let organizationName = "NDGL-iOS"
+ public static let deploymentTarget = DeploymentTargets.iOS("17.0")
+ public static let bundleId = "org.yapp.NDGL"
+ public static let appName = "NDGL"
+ }
+}
diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift
new file mode 100644
index 0000000..73a7f17
--- /dev/null
+++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift
@@ -0,0 +1,66 @@
+//
+// InfoPlist.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public extension Project {
+ static let appInfoPlist: [String: Plist.Value] = [
+ "CFBundleShortVersionString": .string("1.0.0"),
+ "CFBundleDevelopmentRegion": .string("ko"),
+ "CFBundleVersion": .string("1"),
+ "UIUserInterfaceStyle": "Light",
+ "UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait"],
+ "CFBundleIdentifier": .string("\(Environment.bundleId).App"),
+ "CFBundleDisplayName": .string("$(APP_DISPLAY_NAME)"),
+ "UILaunchStoryboardName": .string("LaunchScreen"),
+ "UIApplicationSceneManifest": .dictionary([
+ "UIApplicationSupportsMultipleScenes": .boolean(false),
+ "UISceneConfigurations": .dictionary([
+ "UIWindowSceneSessionRoleApplication": .array([
+ .dictionary([
+ "UISceneConfigurationName": .string("Default Configuration"),
+ "UISceneDelegateClassName": .string("$(PRODUCT_MODULE_NAME).SceneDelegate")
+ ])
+ ])
+ ])
+ ]),
+ "NSAppTransportSecurity": .dictionary([
+ "NSAllowsArbitraryLoads": .boolean(true)
+ ]),
+ "ITSAppUsesNonExemptEncryption": .boolean(false)
+ ]
+
+ static let demoInfoPlist: [String: Plist.Value] = [
+ "CFBundleShortVersionString": .string("1.0.0"),
+ "CFBundleDevelopmentRegion": .string("ko"),
+ "CFBundleVersion": .string("1"),
+ "UIUserInterfaceStyle": "Light",
+ "UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait"],
+ "CFBundleIdentifier": .string("\(Environment.bundleId)"),
+ "CFBundleDisplayName": .string("$(APP_DISPLAY_NAME)"),
+ "UILaunchStoryboardName": .string("LaunchScreen"),
+ "UIApplicationSceneManifest": .dictionary([
+ "UIApplicationSupportsMultipleScenes": .boolean(false),
+ "UISceneConfigurations": .dictionary([
+ "UIWindowSceneSessionRoleApplication": .array([
+ .dictionary([
+ "UISceneConfigurationName": .string("Default Configuration"),
+ "UISceneDelegateClassName": .string("$(PRODUCT_MODULE_NAME).SceneDelegate")
+ ])
+ ])
+ ])
+ ]),
+ "NSAppTransportSecurity": .dictionary([
+ "NSAllowsArbitraryLoads": .boolean(true)
+ ]),
+ "ITSAppUsesNonExemptEncryption": .boolean(false)
+ ]
+
+ static let framework: InfoPlist = .extendingDefault(with: [
+ "CFBundlePackageType": "FMWK"
+ ])
+}
diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift
new file mode 100644
index 0000000..1af74fa
--- /dev/null
+++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift
@@ -0,0 +1,86 @@
+//
+// Settings+Extension.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+public extension Settings {
+ /// 프레임워크용 기본 설정
+ static let frameworkSettings: Settings = .settings(
+ base: [
+ "SKIP_INSTALL": "YES",
+ "BUILD_LIBRARY_FOR_DISTRIBUTION": "YES",
+ "DEFINES_MODULE": "YES",
+ "ENABLE_BITCODE": "NO",
+ "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget),
+ "SWIFT_VERSION": "6.0",
+ "CLANG_ENABLE_MODULES": "YES",
+ "ENABLE_USER_SCRIPT_SANDBOXING": "NO",
+ "ENABLE_MODULE_VERIFIER": "YES",
+ "MODULE_VERIFIER_SUPPORTED_LANGUAGES": "objective-c objective-c++"
+ ]
+ )
+
+ /// 앱용 설정
+ static func appSettings() -> Settings {
+ let baseSettings: [String: SettingValue] = [
+ "APP_NAME": .string(Environment.App.displayName),
+ "APP_DISPLAY_NAME": .string(Environment.App.displayName),
+ "CODE_SIGN_STYLE": "Manual",
+ "DEVELOPMENT_TEAM": SettingValue(stringLiteral: "$(DEVELOPMENT_TEAM)"),
+ "MARKETING_VERSION": .string(Environment.App.version),
+ "CURRENT_PROJECT_VERSION": .string(Environment.App.buildNumber),
+ "ENABLE_BITCODE": "NO",
+ "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget),
+ "SWIFT_VERSION": "6.0",
+ "CLANG_ENABLE_MODULES": "YES",
+ "ENABLE_USER_SCRIPT_SANDBOXING": "NO",
+ "ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS": "YES"
+ ]
+
+ let debugSettings: [String: SettingValue] = [
+ "APP_DISPLAY_NAME": .string("\(Environment.App.displayName)-Dev"),
+ "PRODUCT_NAME": .string("\(Environment.App.displayName)"),
+ "PROVISIONING_PROFILE_SPECIFIER": SettingValue(stringLiteral: "$(APP_PROVISIONING_PROFILE)"),
+ "CODE_SIGN_IDENTITY": SettingValue(stringLiteral: "$(CODE_SIGN_IDENTITY)"),
+ "ENABLE_TESTABILITY": "YES",
+ "GCC_OPTIMIZATION_LEVEL": "0",
+ "SWIFT_OPTIMIZATION_LEVEL": "-Onone",
+ "DEBUG_INFORMATION_FORMAT": "dwarf",
+ "GCC_PREPROCESSOR_DEFINITIONS": .array(["DEBUG=1"])
+ ]
+
+ let releaseSettings: [String: SettingValue] = [
+ "APP_DISPLAY_NAME": .string(Environment.App.displayName),
+ "PRODUCT_NAME": .string("\(Environment.App.displayName)"),
+ "PROVISIONING_PROFILE_SPECIFIER": SettingValue(stringLiteral: "$(APP_PROVISIONING_PROFILE)"),
+ "CODE_SIGN_IDENTITY": SettingValue(stringLiteral: "$(CODE_SIGN_IDENTITY)"),
+ "SWIFT_OPTIMIZATION_LEVEL": "-O",
+ "ENABLE_TESTABILITY": "NO",
+ "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
+ "SWIFT_COMPILATION_MODE": "wholemodule"
+ ]
+
+ return .settings(
+ base: baseSettings,
+ configurations: [
+ .debug(name: .debug, settings: debugSettings),
+ .release(name: .release, settings: releaseSettings)
+ ]
+ )
+ }
+
+ /// 데모 앱용 설정
+ static let demoAppSettings: Settings = .settings(
+ base: [
+ "CODE_SIGN_STYLE": "Automatic",
+ "DEVELOPMENT_TEAM": SettingValue(stringLiteral: "$(DEVELOPMENT_TEAM)"),
+ "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget),
+ "SWIFT_VERSION": "6.0",
+ "ENABLE_TESTABILITY": "YES"
+ ]
+ )
+}
diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift
new file mode 100644
index 0000000..19f7d80
--- /dev/null
+++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Target+Extension.swift
@@ -0,0 +1,67 @@
+//
+// Target+Extension.swift
+// EnvPlugin
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+
+//MARK: - App Target 생성
+public extension Target {
+ static func makeAppTarget(
+ name: String,
+ deploymentTargetsVersion: String = Environment.App.version,
+ infoPlist: [String : Plist.Value],
+ entitlements: String? = nil,
+ scripts: [TargetScript],
+ dependencies: [TargetDependency],
+ settings: Settings
+ ) -> Target {
+ let appTaget: Target = .target(
+ name: name,
+ destinations: [.iPhone],
+ product: .app,
+ bundleId: "\(Environment.bundleId).\(name)",
+ deploymentTargets: .iOS(deploymentTargetsVersion),
+ infoPlist: .extendingDefault(with: infoPlist),
+ sources: ["Sources/**"],
+ resources: ["Resources/**"],
+ entitlements: entitlements.map {
+ .file(path: .relativeToRoot($0))
+ },
+ scripts: scripts,
+ dependencies: dependencies,
+ settings: settings
+ )
+
+ return appTaget
+ }
+}
+
+//MARK: - Framework Target 생성
+public extension Target {
+ static func makeFrameworkTarget(
+ name: String,
+ infoPlist: [String : Plist.Value] = [:],
+ sources: SourceFilesList = ["Sources/**"],
+ dependencies: [TargetDependency],
+ scripts: [TargetScript],
+ isStatic: Bool = false,
+ hasResources: Bool = true
+ ) -> Target {
+ return .target(
+ name: name,
+ destinations: .iOS,
+ product: isStatic ? .staticFramework : .framework,
+ bundleId: "\(Environment.bundleId).\(name)",
+ deploymentTargets: .iOS(Environment.deploymentTarget),
+ infoPlist: .extendingDefault(with: infoPlist),
+ sources: sources,
+ resources: hasResources ? ["Resources/**"] : nil,
+ scripts: scripts,
+ dependencies: dependencies,
+ settings: .frameworkSettings
+ )
+ }
+}
diff --git a/Plugins/TemplatePlugin/Plugin.swift b/Plugins/TemplatePlugin/Plugin.swift
new file mode 100644
index 0000000..6812e2b
--- /dev/null
+++ b/Plugins/TemplatePlugin/Plugin.swift
@@ -0,0 +1,11 @@
+//
+// Plugin.swift
+// ConfigPlugin
+//
+// Created by 최안용 on 1/15/26.
+//
+
+import ProjectDescription
+
+let templatePlugin = Plugin(name: "TemplatePlugin")
+
diff --git a/Plugins/TemplatePlugin/Templates/Feature/Empty.stencil b/Plugins/TemplatePlugin/Templates/Feature/Empty.stencil
new file mode 100644
index 0000000..fd5453b
--- /dev/null
+++ b/Plugins/TemplatePlugin/Templates/Feature/Empty.stencil
@@ -0,0 +1,8 @@
+//
+// Empty.swift
+// Templates
+//
+// Created by {{ author }} on {{ current_date }}
+//
+
+import Foundation
diff --git a/Plugins/TemplatePlugin/Templates/Feature/Feature.swift b/Plugins/TemplatePlugin/Templates/Feature/Feature.swift
new file mode 100644
index 0000000..4510b1e
--- /dev/null
+++ b/Plugins/TemplatePlugin/Templates/Feature/Feature.swift
@@ -0,0 +1,50 @@
+//
+// Feature.swift
+// ConfigPlugin
+//
+// Created by 최안용 on 1/15/26.
+//
+
+import ProjectDescription
+
+let nameAttribute: Template.Attribute = .required("name")
+let author: Template.Attribute = .required("author")
+let currentDate: Template.Attribute = .required("current_date")
+
+let template = Template(
+ description: "Creates a new feature module",
+ attributes: [
+ nameAttribute,
+ author,
+ currentDate
+ ],
+ items: ModuleTemplate.allCases.flatMap{ $0.item }
+)
+
+enum ModuleTemplate: CaseIterable {
+ case main, sources // 추후 tests/demo 등 추가
+
+ var path: String {
+ switch self {
+ case .main:
+ return .basePath
+ case .sources:
+ return .basePath + "/Sources"
+ }
+ }
+
+ var item: [Template.Item] {
+ switch self {
+ case .main:
+ return [.file(path: path + "/Project.swift", templatePath: "Project.stencil")]
+ case .sources:
+ return [.file(path: path + "/Empty.swift", templatePath: "Empty.stencil")]
+ }
+ }
+}
+
+extension String {
+ static var basePath: String {
+ return "Projects/Features/\(nameAttribute)Feature"
+ }
+}
diff --git a/Plugins/TemplatePlugin/Templates/Feature/Project.stencil b/Plugins/TemplatePlugin/Templates/Feature/Project.stencil
new file mode 100644
index 0000000..5caad42
--- /dev/null
+++ b/Plugins/TemplatePlugin/Templates/Feature/Project.stencil
@@ -0,0 +1,25 @@
+//
+// Project.swift
+// ProjectDescriptionHelpers
+//
+// Created by {{ author }} on {{ current_date }}.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "{{name}}Feature",
+ targets: [
+ .makeFrameworkTarget(
+ name: "{{name}}Feature",
+ dependencies: [
+ .Features.baseFeatureDependency
+ ],
+ scripts: [.swiftLint],
+ isStatic: true,
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift
new file mode 100644
index 0000000..eb201ca
--- /dev/null
+++ b/Projects/App/Project.swift
@@ -0,0 +1,29 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import EnvPlugin
+import DependencyPlugin
+import ConfigPlugin
+
+
+let project = Project.makeModule(
+ name: "App",
+ targets: [
+ .makeAppTarget(
+ name: "App",
+ infoPlist: Project.appInfoPlist,
+ scripts: [.swiftLint],
+ dependencies: [
+ .data,
+ .Features.rootFeature
+ ],
+ settings: .appSettings()
+ )
+ ]
+)
diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png
new file mode 100644
index 0000000..105677a
Binary files /dev/null and b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png differ
diff --git a/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..45047c5
--- /dev/null
+++ b/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "AppIcon~ios-marketing.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/App/Resources/Assets.xcassets/Contents.json b/Projects/App/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Projects/App/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/App/Resources/LaunchScreen.storyboard b/Projects/App/Resources/LaunchScreen.storyboard
new file mode 100644
index 0000000..08b3fa0
--- /dev/null
+++ b/Projects/App/Resources/LaunchScreen.storyboard
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Projects/App/Sources/Application/AppDelegate.swift b/Projects/App/Sources/Application/AppDelegate.swift
new file mode 100644
index 0000000..3f64da5
--- /dev/null
+++ b/Projects/App/Sources/Application/AppDelegate.swift
@@ -0,0 +1,40 @@
+//
+// AppDelegate.swift
+// App
+//
+// Created by 최안용 on 1/14/26.
+// Copyright © 2026 NDGL-iOS. All rights reserved.
+//
+
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return true
+ }
+
+ // MARK: UISceneSession Lifecycle
+
+ func application(
+ _ application: UIApplication,
+ configurationForConnecting connectingSceneSession: UISceneSession,
+ options: UIScene.ConnectionOptions
+ ) -> UISceneConfiguration {
+ return UISceneConfiguration(
+ name: "Default Configuration",
+ sessionRole: connectingSceneSession.role
+ )
+ }
+
+ func application(
+ _ application: UIApplication,
+ didDiscardSceneSessions sceneSessions: Set
+ ) {
+
+ }
+}
+
diff --git a/Projects/App/Sources/Application/SceneDelegate.swift b/Projects/App/Sources/Application/SceneDelegate.swift
new file mode 100644
index 0000000..7d847a1
--- /dev/null
+++ b/Projects/App/Sources/Application/SceneDelegate.swift
@@ -0,0 +1,49 @@
+//
+// SceneDelegate.swift
+// App
+//
+// Created by 최안용 on 1/14/26.
+// Copyright © 2026 NDGL-iOS. All rights reserved.
+//
+
+import UIKit
+
+import Core
+import RootFeature
+import BaseFeatureDependency
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(
+ _ scene: UIScene,
+ willConnectTo session: UISceneSession,
+ options connectionOptions: UIScene.ConnectionOptions
+ ) {
+ guard let windowScene = (scene as? UIWindowScene) else { return }
+ window = UIWindow(windowScene: windowScene)
+ window?.rootViewController = RootVC()
+ window?.makeKeyAndVisible()
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+
+ }
+}
+
diff --git a/Projects/Core/Project.swift b/Projects/Core/Project.swift
new file mode 100644
index 0000000..12cd60a
--- /dev/null
+++ b/Projects/Core/Project.swift
@@ -0,0 +1,24 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "Core",
+ targets: [
+ .makeFrameworkTarget(
+ name: "Core",
+ dependencies: [
+ .Modules.thirdPartyLibs
+ ],
+ scripts: [.swiftLint],
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/Core/Sources/Extensions/UIKit+/Empty.swift b/Projects/Core/Sources/Extensions/UIKit+/Empty.swift
new file mode 100644
index 0000000..3423f2b
--- /dev/null
+++ b/Projects/Core/Sources/Extensions/UIKit+/Empty.swift
@@ -0,0 +1,8 @@
+//
+// Empty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import UIKit
diff --git a/Projects/Data/Project.swift b/Projects/Data/Project.swift
new file mode 100644
index 0000000..e59eccf
--- /dev/null
+++ b/Projects/Data/Project.swift
@@ -0,0 +1,27 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "Data",
+ targets: [
+ .makeFrameworkTarget(
+ name: "Data",
+ dependencies: [
+ .domain,
+ .Modules.networks
+ ],
+ scripts: [.swiftLint],
+ isStatic: true,
+ hasResources: false
+ )
+ ]
+)
+
diff --git a/Projects/Data/Sources/Repository/RepoEmpty.swift b/Projects/Data/Sources/Repository/RepoEmpty.swift
new file mode 100644
index 0000000..17afcba
--- /dev/null
+++ b/Projects/Data/Sources/Repository/RepoEmpty.swift
@@ -0,0 +1,8 @@
+//
+// RepoEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Data/Sources/Transform/TransEmpty.swift b/Projects/Data/Sources/Transform/TransEmpty.swift
new file mode 100644
index 0000000..f83abee
--- /dev/null
+++ b/Projects/Data/Sources/Transform/TransEmpty.swift
@@ -0,0 +1,8 @@
+//
+// TransEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Domain/Project.swift b/Projects/Domain/Project.swift
new file mode 100644
index 0000000..e6bd7da
--- /dev/null
+++ b/Projects/Domain/Project.swift
@@ -0,0 +1,24 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "Domain",
+ targets: [
+ .makeFrameworkTarget(
+ name: "Domain",
+ dependencies: [
+ .core
+ ],
+ scripts: [.swiftLint],
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/Domain/Sources/Interface/InterfaceEmpty.swift b/Projects/Domain/Sources/Interface/InterfaceEmpty.swift
new file mode 100644
index 0000000..c2e3ea8
--- /dev/null
+++ b/Projects/Domain/Sources/Interface/InterfaceEmpty.swift
@@ -0,0 +1,8 @@
+//
+// InterfaceEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Domain/Sources/Model/ModelEmpty.swift b/Projects/Domain/Sources/Model/ModelEmpty.swift
new file mode 100644
index 0000000..a2f2190
--- /dev/null
+++ b/Projects/Domain/Sources/Model/ModelEmpty.swift
@@ -0,0 +1,8 @@
+//
+// ModelEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Domain/Sources/UseCase/UsecaseEmpty.swift b/Projects/Domain/Sources/UseCase/UsecaseEmpty.swift
new file mode 100644
index 0000000..42a520b
--- /dev/null
+++ b/Projects/Domain/Sources/UseCase/UsecaseEmpty.swift
@@ -0,0 +1,8 @@
+//
+// UsecaseEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Features/BaseFeatureDependency/Project.swift b/Projects/Features/BaseFeatureDependency/Project.swift
new file mode 100644
index 0000000..584bfc9
--- /dev/null
+++ b/Projects/Features/BaseFeatureDependency/Project.swift
@@ -0,0 +1,26 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/15/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "BaseFeatureDependency",
+ targets: [
+ .makeFrameworkTarget(
+ name: "BaseFeatureDependency",
+ dependencies: [
+ .domain,
+ .Modules.dsKit
+ ],
+ scripts: [.swiftLint],
+ hasResources: false
+ )
+ ]
+)
+
diff --git a/Projects/Features/BaseFeatureDependency/Sources/BaseEmpty.swift b/Projects/Features/BaseFeatureDependency/Sources/BaseEmpty.swift
new file mode 100644
index 0000000..6d09b52
--- /dev/null
+++ b/Projects/Features/BaseFeatureDependency/Sources/BaseEmpty.swift
@@ -0,0 +1,8 @@
+//
+// BaseEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/15/26.
+//
+
+import Foundation
diff --git a/Projects/Features/HomeFeature/Project.swift b/Projects/Features/HomeFeature/Project.swift
new file mode 100644
index 0000000..4c3baad
--- /dev/null
+++ b/Projects/Features/HomeFeature/Project.swift
@@ -0,0 +1,26 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/14/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "HomeFeature",
+ targets: [
+ .makeFrameworkTarget(
+ name: "HomeFeature",
+ dependencies: [
+ .Features.baseFeatureDependency
+ ],
+ scripts: [.swiftLint],
+ isStatic: true,
+ hasResources: false
+ )
+ ]
+)
+
diff --git a/Projects/Features/HomeFeature/Sources/HomeVC.swift b/Projects/Features/HomeFeature/Sources/HomeVC.swift
new file mode 100644
index 0000000..c2ebe59
--- /dev/null
+++ b/Projects/Features/HomeFeature/Sources/HomeVC.swift
@@ -0,0 +1,37 @@
+//
+// HomeVC.swift
+// HomeFeature
+//
+// Created by 최안용 on 1/15/26.
+// Copyright © 2026 NDGL-iOS. All rights reserved.
+//
+
+import UIKit
+
+import Core
+import Domain
+import DSKit
+
+import BaseFeatureDependency
+
+// Coordinator 도입시 public 제거 후 Coordinator 클래스를 통해 접근
+public final class HomeVC: UIViewController {
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = .red
+ }
+
+
+ /*
+ // MARK: - Navigation
+
+ // In a storyboard-based application, you will often want to do a little preparation before navigation
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+ // Get the new view controller using segue.destination.
+ // Pass the selected object to the new view controller.
+ }
+ */
+
+}
diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift
new file mode 100644
index 0000000..cc68da1
--- /dev/null
+++ b/Projects/Features/RootFeature/Project.swift
@@ -0,0 +1,26 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "RootFeature",
+ targets: [
+ .makeFrameworkTarget(
+ name: "RootFeature",
+ dependencies: [
+ .Features.Home.feature
+ ],
+ scripts: [.swiftLint],
+ isStatic: true,
+ hasResources: false
+ )
+ ]
+)
+
diff --git a/Projects/Features/RootFeature/Sources/RootVC.swift b/Projects/Features/RootFeature/Sources/RootVC.swift
new file mode 100644
index 0000000..6e2290b
--- /dev/null
+++ b/Projects/Features/RootFeature/Sources/RootVC.swift
@@ -0,0 +1,33 @@
+//
+// RootVC.swift
+// RootFeature
+//
+// Created by 최안용 on 1/15/26.
+// Copyright © 2026 NDGL-iOS. All rights reserved.
+//
+
+import UIKit
+
+import Core
+import BaseFeatureDependency
+import HomeFeature
+
+
+public final class RootVC: UIViewController {
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let homeNavigationController = UINavigationController()
+ homeNavigationController.viewControllers = [HomeVC()]
+
+ addChild(homeNavigationController)
+
+ view.addSubview(homeNavigationController.view)
+
+ homeNavigationController.view.frame = view.bounds
+ homeNavigationController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+
+ homeNavigationController.didMove(toParent: self)
+ }
+}
diff --git a/Projects/Modules/DSKit/Project.swift b/Projects/Modules/DSKit/Project.swift
new file mode 100644
index 0000000..f993ce1
--- /dev/null
+++ b/Projects/Modules/DSKit/Project.swift
@@ -0,0 +1,24 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "DSKit",
+ targets: [
+ .makeFrameworkTarget(
+ name: "DSKit",
+ dependencies: [
+ .core
+ ],
+ scripts: [.swiftLint],
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/Modules/DSKit/Sources/Component/DSCEmpty.swift b/Projects/Modules/DSKit/Sources/Component/DSCEmpty.swift
new file mode 100644
index 0000000..de7d7d8
--- /dev/null
+++ b/Projects/Modules/DSKit/Sources/Component/DSCEmpty.swift
@@ -0,0 +1,8 @@
+//
+// DSCEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/DSKit/Sources/DSEmpty.swift b/Projects/Modules/DSKit/Sources/DSEmpty.swift
new file mode 100644
index 0000000..a9b76b7
--- /dev/null
+++ b/Projects/Modules/DSKit/Sources/DSEmpty.swift
@@ -0,0 +1,8 @@
+//
+// DSEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift
new file mode 100644
index 0000000..2414eb4
--- /dev/null
+++ b/Projects/Modules/Networks/Project.swift
@@ -0,0 +1,25 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "Networks",
+ targets: [
+ .makeFrameworkTarget(
+ name: "Networks",
+ dependencies: [
+ .core
+ ],
+ scripts: [.swiftLint],
+ isStatic: true,
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/Modules/Networks/Sources/API/APIEmpty.swift b/Projects/Modules/Networks/Sources/API/APIEmpty.swift
new file mode 100644
index 0000000..cc161f1
--- /dev/null
+++ b/Projects/Modules/Networks/Sources/API/APIEmpty.swift
@@ -0,0 +1,8 @@
+//
+// APIEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift b/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift
new file mode 100644
index 0000000..c0b8f52
--- /dev/null
+++ b/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift
@@ -0,0 +1,8 @@
+//
+// NBaseEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift b/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift
new file mode 100644
index 0000000..be4230d
--- /dev/null
+++ b/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift
@@ -0,0 +1,8 @@
+//
+// EntityEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift b/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift
new file mode 100644
index 0000000..a45ef69
--- /dev/null
+++ b/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift
@@ -0,0 +1,8 @@
+//
+// ServiceEmpty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Projects/Modules/ThirdPartyLibs/Project.swift b/Projects/Modules/ThirdPartyLibs/Project.swift
new file mode 100644
index 0000000..411b945
--- /dev/null
+++ b/Projects/Modules/ThirdPartyLibs/Project.swift
@@ -0,0 +1,29 @@
+//
+// Project.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import ProjectDescriptionHelpers
+import DependencyPlugin
+
+let project = Project.makeModule(
+ name: "ThirdPartyLibs",
+ targets: [
+ .makeFrameworkTarget(
+ name: "ThirdPartyLibs",
+ dependencies: [
+ .SPM.Kingfisher,
+ .SPM.Moya,
+ .SPM.RIBs,
+ .SPM.RxSwift,
+ .SPM.SnapKit,
+ .SPM.Then
+ ],
+ scripts: [],
+ hasResources: false
+ )
+ ]
+)
diff --git a/Projects/Modules/ThirdPartyLibs/Sources/Empty.swift b/Projects/Modules/ThirdPartyLibs/Sources/Empty.swift
new file mode 100644
index 0000000..7063f0b
--- /dev/null
+++ b/Projects/Modules/ThirdPartyLibs/Sources/Empty.swift
@@ -0,0 +1,8 @@
+//
+// Empty.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import Foundation
diff --git a/Scripts/GenerateModule.swift b/Scripts/GenerateModule.swift
new file mode 100644
index 0000000..3ab4e6b
--- /dev/null
+++ b/Scripts/GenerateModule.swift
@@ -0,0 +1,60 @@
+#!/user/bin/swift
+import Foundation
+
+// 1. 유틸리티 함수: 날짜 가져오기
+func getCurrentDate() -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy/MM/dd"
+ return formatter.string(from: Date())
+}
+
+// 2. 입력 받기
+print("🚀 새 Feature 모듈 생성을 시작합니다.")
+
+print("입력: 모듈 이름 (예: Home, MyPage)", terminator: " : ")
+guard let name = readLine(), !name.isEmpty else {
+ print("❌ 모듈 이름은 필수입니다."); exit(1)
+}
+
+print("입력: 작성자 이름", terminator: " : ")
+guard let author = readLine(), !author.isEmpty else {
+ print("❌ 작성자 이름은 필수입니다."); exit(1)
+}
+
+let currentDate = getCurrentDate()
+
+// 3. 실행할 tuist scaffold 명령어 구성
+// 템플릿 이름이 'feature'라고 가정합니다. (Template.swift가 있는 폴더명)
+let command = [
+ "tuist", "scaffold", "Feature",
+ "--name", name,
+ "--author", author,
+ "--current_date", currentDate
+]
+
+print("\n-------------------------------------------")
+print("생성 정보 확인")
+print("- 모듈명: \(name)Feature")
+print("- 작성자: \(author)")
+print("- 날짜: \(currentDate)")
+print("-------------------------------------------\n")
+
+// 4. 프로세스 실행
+let process = Process()
+process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
+process.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+process.arguments = command
+
+do {
+ try process.run()
+ process.waitUntilExit()
+
+ if process.terminationStatus == 0 {
+ print("\n✅ \(name)Feature 모듈이 성공적으로 생성되었습니다!")
+ print("💡 'tuist generate'를 실행하여 프로젝트를 갱신하세요.")
+ } else {
+ print("\n❌ 모듈 생성 중 오류가 발생했습니다. (Status: \(process.terminationStatus))")
+ }
+} catch {
+ print("❌ 명령어 실행 실패: \(error)")
+}
diff --git a/Scripts/Onboarding.sh b/Scripts/Onboarding.sh
new file mode 100755
index 0000000..672fc56
--- /dev/null
+++ b/Scripts/Onboarding.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+# 에러 발생 시 즉시 중단
+set -e
+
+echo -e "🚀 Onboarding Start\n"
+
+# 0. 실행 경로 고정 (프로젝트 루트)
+cd "$(dirname "$0")/.."
+
+# ---------------------------------------------------------
+# 1. PrivateFile 레포지토리를 통한 보안 파일 자동 세팅
+# ---------------------------------------------------------
+
+PRIVATE_REPO_URL="https://github.com/ChoiAnYong/fastlane-match.git"
+TEMP_DIR="temp_private_configs"
+
+echo "🔐 보안 설정 파일을 가져오는 중 (PrivateFile 레포지토리)..."
+
+# 기존에 폴더가 있다면 삭제 후 새로 클론
+rm -rf "$TEMP_DIR"
+git clone "$PRIVATE_REPO_URL" "$TEMP_DIR"
+
+# 레포지토리 내 PrivateFile 폴더가 있는지 확인 후 경로 설정
+SOURCE_PATH="$TEMP_DIR/PrivateFile"
+if [ ! -d "$SOURCE_PATH" ]; then
+ SOURCE_PATH="$TEMP_DIR"
+fi
+
+echo "📁 설정 파일 배치 중..."
+mkdir -p xcconfigs
+mkdir -p fastlane
+
+cp "$SOURCE_PATH/Debug.xcconfig" xcconfigs/ 2>/dev/null || echo "⚠️ Debug.xcconfig missing"
+cp "$SOURCE_PATH/Release.xcconfig" xcconfigs/ 2>/dev/null || echo "⚠️ Release.xcconfig missing"
+cp "$SOURCE_PATH/.env.default" fastlane/ 2>/dev/null || echo "⚠️ .env.default missing"
+# entitlements 파일이 있다면 추가 (필요시 주석 해제)
+# cp "$SOURCE_PATH/NDGL.entitlements" xcconfigs/ 2>/dev/null || echo "⚠️ NDGL.entitlements missing"
+
+# 임시 폴더 삭제
+rm -rf "$TEMP_DIR"
+
+# ---------------------------------------------------------
+# 2. 필수 설정 파일 검증
+# ---------------------------------------------------------
+REQUIRED_FILES=(
+ "fastlane/.env.default"
+ "xcconfigs/Debug.xcconfig"
+ "xcconfigs/Release.xcconfig"
+)
+
+echo "🔍 가져온 파일 검증 중..."
+MISSING_FILES=()
+
+for FILE in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$FILE" ]; then
+ MISSING_FILES+=("$FILE")
+ fi
+done
+
+if [ ${#MISSING_FILES[@]} -ne 0 ]; then
+ echo -e "\n❌ [Error] 아래 필수 파일이 누락되었습니다:"
+ for MISSING in "${MISSING_FILES[@]}"; do
+ echo " - $MISSING"
+ done
+ exit 1
+fi
+
+echo -e "✅ 모든 설정 파일이 정상적으로 배치되었습니다.\n"
+
+# ---------------------------------------------------------
+# 3. 환경 변수 로드
+# ---------------------------------------------------------
+export $(grep -v '^#' fastlane/.env.default | xargs)
+
+# ---------------------------------------------------------
+# 4. mise 설치 및 활성화 (Bash 버전)
+# ---------------------------------------------------------
+if ! command -v mise &> /dev/null; then
+ echo -e "[mise] Installing mise..."
+ brew install mise
+fi
+# 중요: zsh가 아닌 bash로 활성화
+eval "$(mise activate bash)"
+
+# 5. 도구 설치
+if [ -f .mise.toml ]; then
+ echo -e "[mise] Installing tools from .mise.toml..."
+ mise install
+fi
+
+# ---------------------------------------------------------
+# 6. rbenv 및 Ruby 설정 (Bash 버전)
+# ---------------------------------------------------------
+if ! command -v rbenv &> /dev/null; then
+ echo -e "[rbenv] Installing rbenv..."
+ brew install rbenv
+fi
+export PATH="$HOME/.rbenv/bin:$PATH"
+# 중요: bash로 초기화
+eval "$(rbenv init - bash)"
+
+RUBY_VERSION=$(cat .ruby-version 2>/dev/null || echo "3.2.2")
+if ! rbenv versions | grep -q "$RUBY_VERSION"; then
+ echo -e "[rbenv] Installing Ruby $RUBY_VERSION..."
+ rbenv install "$RUBY_VERSION"
+fi
+rbenv local "$RUBY_VERSION"
+rbenv rehash
+
+# ---------------------------------------------------------
+# 7. Bundler 및 의존성 설치
+# ---------------------------------------------------------
+echo -e "[Bundler] Installing gems..."
+gem install bundler --no-document
+bundle install
+
+# ---------------------------------------------------------
+# 8. Tuist 설정
+# ---------------------------------------------------------
+echo -e "\n📦 Tuist Setting..."
+if [ -f Makefile ]; then
+ # Makefile의 generate 타겟 실행 (tuist install/generate 포함 권장)
+ make generate
+else
+ tuist install
+ tuist generate
+fi
+
+# ---------------------------------------------------------
+# 9. Fastlane Match
+# ---------------------------------------------------------
+echo -e "\n🔐 Installing Certificates..."
+bundle exec fastlane match development --readonly
+bundle exec fastlane match appstore --readonly
+
+echo -e "\n✅ 온보딩 완료! 이제 Xcode에서 프로젝트를 빌드할 수 있습니다."
diff --git a/Tuist.swift b/Tuist.swift
new file mode 100644
index 0000000..eefebbb
--- /dev/null
+++ b/Tuist.swift
@@ -0,0 +1,11 @@
+import ProjectDescription
+
+let tuist = Tuist(project: .tuist(
+ plugins: [
+ .local(path: .relativeToRoot("Plugins/DependencyPlugin")),
+ .local(path: .relativeToRoot("Plugins/ConfigPlugin")),
+ .local(path: .relativeToRoot("Plugins/EnvPlugin")),
+ .local(path: .relativeToRoot("Plugins/TemplatePlugin"))
+ ],
+ generationOptions: .options()
+))
diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved
new file mode 100644
index 0000000..d5a5d8f
--- /dev/null
+++ b/Tuist/Package.resolved
@@ -0,0 +1,78 @@
+{
+ "originHash" : "d7518f58a4cae139e84fb7d8fe5dd3abeb7b75bb9b7260d32354d7473b9a6b84",
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire.git",
+ "state" : {
+ "revision" : "7be73f6c2b5cd90e40798b06ebd5da8f9f79cf88",
+ "version" : "5.11.0"
+ }
+ },
+ {
+ "identity" : "kingfisher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/onevcat/Kingfisher.git",
+ "state" : {
+ "revision" : "d30a5fad881137e2267f96a8e3fc35c58999bb94",
+ "version" : "8.6.2"
+ }
+ },
+ {
+ "identity" : "moya",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Moya/Moya.git",
+ "state" : {
+ "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
+ "version" : "15.0.3"
+ }
+ },
+ {
+ "identity" : "reactiveswift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
+ "state" : {
+ "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
+ "version" : "6.7.0"
+ }
+ },
+ {
+ "identity" : "ribs-ios",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/uber/RIBs-iOS.git",
+ "state" : {
+ "revision" : "53f7cb391d48c385730df6ce1fb95029bca9d25a",
+ "version" : "1.0.0"
+ }
+ },
+ {
+ "identity" : "rxswift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/ReactiveX/RxSwift.git",
+ "state" : {
+ "revision" : "5004a18539bd68905c5939aa893075f578f4f03d",
+ "version" : "6.9.1"
+ }
+ },
+ {
+ "identity" : "snapkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SnapKit/SnapKit.git",
+ "state" : {
+ "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
+ "version" : "5.7.1"
+ }
+ },
+ {
+ "identity" : "then",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/devxoul/Then",
+ "state" : {
+ "revision" : "e421a7b3440a271834337694e6050133a3958bc7",
+ "version" : "2.7.0"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/Tuist/Package.swift b/Tuist/Package.swift
new file mode 100644
index 0000000..69ab405
--- /dev/null
+++ b/Tuist/Package.swift
@@ -0,0 +1,26 @@
+// swift-tools-version: 6.0
+import PackageDescription
+
+#if TUIST
+ import struct ProjectDescription.PackageSettings
+
+ let packageSettings = PackageSettings(
+ // Customize the product types for specific package product
+ // Default is .staticFramework
+ // productTypes: ["Alamofire": .framework,]
+ productTypes: [:]
+ )
+#endif
+
+let package = Package(
+ name: "NDGL-iOS",
+ dependencies: [
+ .package(url: "https://github.com/Moya/Moya.git", from: "15.0.0"),
+ .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.1"),
+ .package(url: "https://github.com/devxoul/Then", from: "2.0.0"),
+ .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"),
+ .package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0"),
+ .package(url: "https://github.com/uber/RIBs-iOS.git", from: "1.0.0")
+ ]
+)
+
diff --git a/Tuist/ProjectDescriptionHelpers/Project+MakeModule.swift b/Tuist/ProjectDescriptionHelpers/Project+MakeModule.swift
new file mode 100644
index 0000000..4221ef3
--- /dev/null
+++ b/Tuist/ProjectDescriptionHelpers/Project+MakeModule.swift
@@ -0,0 +1,49 @@
+//
+// Project+MakeModule.swift
+// 27th-App-Team-1-iOSManifests
+//
+// Created by 최안용 on 1/13/26.
+//
+
+import ProjectDescription
+import EnvPlugin
+import ConfigPlugin
+
+extension Project {
+ public static func makeModule(
+ name: String,
+ organizationName: String = Environment.organizationName,
+ packages: [Package] = [],
+ settings: Settings? = nil,
+ targets: [Target] = [],
+ schemes: [Scheme]? = nil,
+ fileHeaderTemplate: FileHeaderTemplate? = nil,
+ additionalFiles: [FileElement] = [],
+ resourceSynthesizers: [ResourceSynthesizer] = .default
+ ) -> Project {
+ if let schemes = schemes {
+ return Project(
+ name: name,
+ organizationName: organizationName,
+ packages: packages,
+ settings: .settings(configurations: XCConfig.prod),
+ targets: targets,
+ schemes: schemes,
+ fileHeaderTemplate: fileHeaderTemplate,
+ additionalFiles: additionalFiles,
+ resourceSynthesizers: resourceSynthesizers
+ )
+ } else {
+ return Project(
+ name: name,
+ organizationName: organizationName,
+ packages: packages,
+ settings: .settings(configurations: XCConfig.prod),
+ targets: targets,
+ fileHeaderTemplate: fileHeaderTemplate,
+ additionalFiles: additionalFiles,
+ resourceSynthesizers: resourceSynthesizers
+ )
+ }
+ }
+}
diff --git a/Tuist/ProjectDescriptionHelpers/TargetScript+Extension.swift b/Tuist/ProjectDescriptionHelpers/TargetScript+Extension.swift
new file mode 100644
index 0000000..bc0725b
--- /dev/null
+++ b/Tuist/ProjectDescriptionHelpers/TargetScript+Extension.swift
@@ -0,0 +1,39 @@
+//
+// TargetScript+Extension.swift
+// ProjectDescriptionHelpers
+//
+// Created by 최안용 on 1/15/26.
+//
+
+import ProjectDescription
+
+public extension TargetScript {
+ static let swiftLint = TargetScript.pre(
+ script: """
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
+
+ SWIFTLINT="$(command -v swiftlint || true)"
+ if [ -z "$SWIFTLINT" ]; then
+ echo "warning: SwiftLint not installed (skipping)"
+ exit 0
+ fi
+
+ ROOT="$SRCROOT"
+ while [ "$ROOT" != "/" ] && [ ! -f "$ROOT/.swiftlint.yml" ]; do
+ ROOT="$(dirname "$ROOT")"
+ done
+
+ if [ ! -f "$ROOT/.swiftlint.yml" ]; then
+ echo "warning: .swiftlint.yml not found (skipping)"
+ exit 0
+ fi
+
+ echo "SwiftLint: $SWIFTLINT"
+ echo "Config: $ROOT/.swiftlint.yml"
+
+ "$SWIFTLINT" lint --config "$ROOT/.swiftlint.yml"
+ """,
+ name: "SwiftLint",
+ basedOnDependencyAnalysis: false
+ )
+}
diff --git a/Workspace.swift b/Workspace.swift
new file mode 100644
index 0000000..8f9c68f
--- /dev/null
+++ b/Workspace.swift
@@ -0,0 +1,8 @@
+import ProjectDescription
+
+let workspace = Workspace(
+ name: "NDGL-iOS",
+ projects: [
+ "Projects/**"
+ ]
+)
diff --git a/fastlane/Appfile b/fastlane/Appfile
new file mode 100644
index 0000000..42682db
--- /dev/null
+++ b/fastlane/Appfile
@@ -0,0 +1,8 @@
+app_identifier(ENV['APP_IDENTIFIER']) # The bundle identifier of your app
+apple_id(ENV['APPLE_ID']) # Your Apple Developer Portal username
+
+itc_team_id(ENV['FASTLANE_ITC_TEAM_ID']) # App Store Connect Team ID
+team_id(ENV['TEAM_ID']) # Developer Portal Team ID
+
+# For more information about the Appfile, see:
+# https://docs.fastlane.tools/advanced/#appfile
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
new file mode 100644
index 0000000..ee0346e
--- /dev/null
+++ b/fastlane/Fastfile
@@ -0,0 +1,92 @@
+# This file contains the fastlane.tools configuration
+# You can find the documentation at https://docs.fastlane.tools
+#
+# For a list of all available actions, check out
+#
+# https://docs.fastlane.tools/actions
+#
+# For a list of all available plugins, check out
+#
+# https://docs.fastlane.tools/plugins/available-plugins
+#
+
+# Uncomment the line if you want fastlane to automatically update itself
+# update_fastlane
+
+default_platform(:ios)
+
+platform :ios do
+
+ # ---------------------------------------------------------
+ # 1. CI 전용 레인 (PR 시 빌드 확인용)
+ # ---------------------------------------------------------
+ desc "Check Build (CI)"
+ lane :ci_check do
+ begin
+ xcodebuild(
+ workspace: "NDGL-iOS.xcworkspace",
+ scheme: "App",
+ configuration: "Debug",
+ sdk: "iphonesimulator",
+ xcargs: "CODE_SIGNING_ALLOWED=NO"
+ )
+ # 성공 시 CI용 메시지 전송
+ send_discord_message(message: "CI 빌드 체크 성공!", success: true, type: "CI")
+ rescue => exception
+ # 실패 시 에러 내용과 함께 알림
+ send_discord_message(message: "CI 빌드 실패: #{exception}", success: false, type: "CI")
+ raise exception
+ end
+ end
+
+ # ---------------------------------------------------------
+ # 2. 배포 전용 레인 (App Store / TestFlight 업로드)
+ # ---------------------------------------------------------
+ desc "Push a new release build to the App Store"
+ lane :release do
+ begin
+ # match(type: "appstore", readonly: true) # CI 환경이라면 추가 권장
+ build_app(scheme: "NDGL-iOS-Workspace")
+ upload_to_app_store(skip_metadata: true, skip_screenshots: true)
+
+ send_discord_message(message: "TestFlight 업로드 완료! 테스터 알림이 전송되었습니다. 🚀", success: true, type: "배포")
+ rescue => exception
+ send_discord_message(message: "배포 실패 에러: #{exception}", success: false, type: "배포")
+ raise exception
+ end
+ end
+
+ # ---------------------------------------------------------
+ # 3. 기기 관리 (로컬용)
+ # ---------------------------------------------------------
+ desc "Register Devices"
+ lane :register_new_device do |options|
+ device_name = prompt(text: "Enter the device name: ")
+ device_udid = prompt(text: "Enter the device UDID: ")
+ register_devices(devices: { device_name => device_udid })
+ match(type: "development", force_for_new_devices: true)
+ end
+end
+
+# ---------------------------------------------------------
+# 🔔 Discord 메시지 전송 함수 (공통)
+# ---------------------------------------------------------
+# ---------------------------------------------------------
+# 🔔 Discord 메시지 전송 함수 (공통)
+# ---------------------------------------------------------
+def send_discord_message(message:, success:, type: "CI")
+ project_path = "./Projects/App/App.xcodeproj"
+
+ version = get_version_number(xcodeproj: project_path)
+ build_number = get_build_number(xcodeproj: project_path)
+
+ emoji = success ? "✅" : "🚨"
+ title_text = success ? "#{type} 성공" : "#{type} 실패"
+
+ discord_notifier(
+ webhook_url: ENV["DISCORD_URL"],
+ title: "#{emoji} #{title_text} — v#{version} (#{build_number})",
+ description: message,
+ success: success
+ )
+end
\ No newline at end of file
diff --git a/fastlane/Matchfile b/fastlane/Matchfile
new file mode 100644
index 0000000..604f000
--- /dev/null
+++ b/fastlane/Matchfile
@@ -0,0 +1,13 @@
+git_url(ENV['REPOSITORY'])
+
+type "development" # The default type, can be: appstore, adhoc or development
+
+app_identifier(ENV['APP_IDENTIFIER'])
+team_id(ENV['TEAM_ID'])
+git_branch(ENV['BRANCH'])
+
+# app_identifier ["tools.fastlane.app", "tools.fastlane.app2"]
+# username "user@fastlane.tools" # Your Apple Developer Portal username
+
+# For all available options run `match --help`
+# Remove the # in the beginning of the line to enable the other options
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
new file mode 100644
index 0000000..7ef2b02
--- /dev/null
+++ b/fastlane/Pluginfile
@@ -0,0 +1,5 @@
+# Autogenerated by fastlane
+#
+# Ensure this file is checked in to source control!
+
+gem 'fastlane-plugin-discord_notifier'
diff --git a/fastlane/README.md b/fastlane/README.md
new file mode 100644
index 0000000..0b13b9b
--- /dev/null
+++ b/fastlane/README.md
@@ -0,0 +1,27 @@
+fastlane documentation
+================
+# Installation
+```
+sudo gem install fastlane
+```
+# Available Actions
+### register_new_device
+```
+fastlane register_new_device
+```
+Register Devices
+
+----
+
+## iOS
+### ios release
+```
+fastlane ios release
+```
+Push a new release build to the App Store
+
+----
+
+This README.md is auto-generated and will be re-generated every time to run [fastlane](https://fastlane.tools).
+More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools).
+The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane/tree/master/fastlane).
\ No newline at end of file
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 0000000..eb98974
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,2 @@
+[tools]
+tuist = "4.118.1"
diff --git a/xcconfigs/NDGL.entitlements b/xcconfigs/NDGL.entitlements
new file mode 100644
index 0000000..98331b3
--- /dev/null
+++ b/xcconfigs/NDGL.entitlements
@@ -0,0 +1 @@
+dk