Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6cd9430
add: #27 - AppIcon 추가
ChoiAnYong Feb 19, 2026
b5a727b
add: #27 - icon 추가
ChoiAnYong Feb 19, 2026
f621801
chore: #27 - 네이밍 오류 수정
ChoiAnYong Feb 19, 2026
01ea00c
feat: #27 - templatesSearch api 연결
ChoiAnYong Feb 19, 2026
d69609e
feat: #27 - ControlEvent 및 focus 구현
ChoiAnYong Feb 20, 2026
c15d13c
design: #27 - EmptyView 구현
ChoiAnYong Feb 20, 2026
10ff3bf
feat: #27 - hideKeyboard 구현
ChoiAnYong Feb 20, 2026
46d8ad7
feat: #27 - SearchRIBs 구현
ChoiAnYong Feb 20, 2026
5499b48
feat: #27 - 자식 RIBs SearchResult RIBs 구현
ChoiAnYong Feb 20, 2026
03011c9
feat: #27 - searchUsecase 의존성 주입
ChoiAnYong Feb 20, 2026
0366fe5
fix: #27 - 옵셔널 제거
ChoiAnYong Feb 20, 2026
cd57a20
fix: #27 - Search 모듈 push시 애니메이션 삭제
ChoiAnYong Feb 20, 2026
c815f59
feat: #27 - route 액션 연결
ChoiAnYong Feb 20, 2026
a913109
design: #27 - LaunchScreen 구현
ChoiAnYong Feb 20, 2026
9e9f6ca
design: #27 - 임시 색상 변경
ChoiAnYong Feb 20, 2026
2371266
feat: #27 - URLHelper 구현
ChoiAnYong Feb 20, 2026
35e412f
design: #27 - Toast 구현
ChoiAnYong Feb 20, 2026
bd69f96
feat: #27 - 임시 uuid, nickname 저장
ChoiAnYong Feb 20, 2026
fdf4f82
feat: #27 - Setting 로직 구현
ChoiAnYong Feb 20, 2026
324c9c8
feat: #27 - apiKey 추가
ChoiAnYong Feb 20, 2026
6976e21
feat: #27 - apiKey 추가
ChoiAnYong Feb 20, 2026
7b00c43
fix: #27 - 환경 변수 오류 수정
ChoiAnYong Feb 20, 2026
ad4ecba
fix: #27 - InfoPlist 누락 수정
ChoiAnYong Feb 20, 2026
bbce5b0
fix: #27 - 헤더명 오류 수정
ChoiAnYong Feb 20, 2026
1d42a4c
fix: #27 - 코드리뷰 반영
ChoiAnYong Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public extension Project {
"NSAllowsArbitraryLoads": .boolean(true)
]),
"ITSAppUsesNonExemptEncryption": .boolean(false),
"BASE_URL": .string("$(BASE_URL)")
"BASE_URL": .string("$(BASE_URL)"),
"X_API_KEY": .string("$(X_API_KEY)")
Comment on lines +35 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify xcconfig gitignore coverage and X_API_KEY definition

echo "=== Checking .gitignore for xcconfig patterns ==="
cat .gitignore 2>/dev/null | grep -E "\.xcconfig|xcconfig/" || echo "No .gitignore or no xcconfig patterns found"

echo ""
echo "=== Finding all xcconfig files in repo ==="
find . -name "*.xcconfig" -type f 2>/dev/null | grep -v node_modules | head -20

echo ""
echo "=== Checking which xcconfig files contain X_API_KEY ==="
find . -name "*.xcconfig" -type f 2>/dev/null -exec grep -l "X_API_KEY" {} \;

echo ""
echo "=== Checking git status for xcconfig files ==="
git ls-files "*.xcconfig" 2>/dev/null || echo "No xcconfig files tracked in git"

echo ""
echo "=== Checking if any xcconfig with X_API_KEY is tracked ==="
for file in $(find . -name "*.xcconfig" -type f 2>/dev/null | grep -v node_modules); do
  if git ls-files --error-unmatch "$file" 2>/dev/null | grep -q .; then
    if grep -q "X_API_KEY" "$file"; then
      echo "⚠️  TRACKED: $file contains X_API_KEY"
    fi
  fi
done
echo "✓ Check complete"

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 361


🏁 Script executed:

#!/bin/bash
# Check for template xcconfig, INFO PLIST context, and X_API_KEY usage

echo "=== Looking for xcconfig templates or examples ==="
find . -type f \( -name "*xcconfig*" -o -name "*Config.template*" \) 2>/dev/null | head -20

echo ""
echo "=== Checking InfoPlist.swift full structure (lines 30-70) ==="
sed -n '30,70p' Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift

echo ""
echo "=== Searching for X_API_KEY usage in codebase ==="
rg "X_API_KEY" --max-count 20

echo ""
echo "=== Searching for xcconfig setup or configuration docs ==="
find . -type f \( -name "*.md" -o -name "*.txt" \) 2>/dev/null -exec grep -l "xcconfig\|X_API_KEY\|BASE_URL" {} \; 2>/dev/null | head -10

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 2825


🏁 Script executed:

#!/bin/bash
# Check NetworkConfiguration implementation and demo target setup

echo "=== NetworkConfiguration.swift around X_API_KEY read ==="
rg -A 5 -B 5 "X_API_KEY.*infoDictionary" Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift

echo ""
echo "=== Check if xcconfig is provided during build phase ==="
find . -type f \( -name "*.swift" -o -name "*.yml" -o -name "*.yaml" \) 2>/dev/null -exec grep -l "xcconfig\|BASE_URL.*inject\|X_API_KEY.*inject" {} \; 2>/dev/null | head -10

echo ""
echo "=== Check Project.swift for xcconfig settings ==="
find . -type f -name "Project.swift" 2>/dev/null | head -5 | xargs -I {} grep -A 10 -B 5 "xcconfig\|BASE_URL\|X_API_KEY" {} 2>/dev/null | head -50

echo ""
echo "=== Check if there's a build configuration documentation ==="
cat README.md 2>/dev/null | grep -A 20 -B 5 "xcconfig\|X_API_KEY\|BASE_URL\|setup\|config" | head -80

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 441


🏁 Script executed:

#!/bin/bash
# Direct file inspection for X_API_KEY handling and xcconfig setup

echo "=== NetworkConfiguration.swift full file ==="
cat -n Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift

echo ""
echo "=== Configurations.swift for xcconfig setup ==="
cat -n Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift

echo ""
echo "=== CI.yml for build environment ==="
head -100 .github/workflows/CI.yml

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 4035


X_API_KEY를 Info.plist에서 조건부로만 읽도록 수정하고, demoInfoPlist에도 추가하세요.

원래 우려했던 xcconfig 보안 설정은 올바릅니다. .gitignore에서 *.xcconfig를 제대로 차단하고 있으며, CI 워크플로우에서 비공개 저장소로부터 해당 파일을 주입하고 있습니다. 그러나 두 가지 실행 문제가 있습니다:

  1. NetworkConfiguration.apiKey (Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift:25-26)는 모든 빌드에서 X_API_KEY를 읽으려 시도합니다. 팀의 지침에 따르면 DEBUG 모드에서만 읽어야 합니다. DEBUG가 아닐 때는 조건부로 처리하거나 경고하도록 수정하세요.

  2. demoInfoPlist (Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift:39-64)에는 X_API_KEY가 정의되어 있지 않습니다. 반면 demo 타겟이 비-DEBUG 빌드에서 AuthAPI, TravelTemplateAPI 등을 호출하면 Info.plist 조회 실패로 런타임 크래시가 발생합니다. demoInfoPlist에도 X_API_KEY 항목을 추가하거나, 해당 APIs 호출을 DEBUG 모드로 제한하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift` around lines 35
- 36, NetworkConfiguration.apiKey is currently attempting to read X_API_KEY on
all builds—change it to only read the plist value in DEBUG (use `#if` DEBUG or a
runtime buildConfiguration check inside NetworkConfiguration.apiKey) and
otherwise return nil or a clear warning/error; update callers to handle a nil
apiKey gracefully. Also add the "X_API_KEY": .string("$(X_API_KEY)") entry to
demoInfoPlist in InfoPlist.swift (the same place where BASE_URL is defined) so
the demo target has the key available in DEBUG builds, or alternatively gate
demo targets' network calls to DEBUG only. Ensure references to
NetworkConfiguration.apiKey and demoInfoPlist are updated to avoid runtime
crashes when the key is absent.

]

static let demoInfoPlist: [String: Plist.Value] = [
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "AppIcon~ios-marketing.png",
"filename" : "AppIcon_Spring_IOS_1024.png",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# PNG 파일 존재 여부 확인
fd -e png "AppIcon_Spring_IOS_1024" --search-path Projects/App/Resources/Assets.xcassets/AppIcon.appiconset

# 전체 AppIcon.appiconset 디렉터리 구조 확인
echo "=== AppIcon.appiconset 디렉터리 내용 ==="
ls -la Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Contents.json 파일의 구조 확인 (dark/tinted 변형 확인)
cat -n Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json | head -40

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 983


AppIcon_Spring_IOS_1024.png 파일 확인됨

새 아이콘 파일이 디렉터리에 존재합니다. 그러나 Dark 및 Tinted 모양 변형에 대해 filename 속성이 누락되어 있습니다. 이들 항목에도 해당하는 PNG 파일을 할당하거나, 다크 모드/틴트 변형을 지원하지 않는다면 이 항목들을 제거하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json` at
line 4, Contents.json currently lists the new icon "AppIcon_Spring_IOS_1024.png"
but the Dark and Tinted appearance variants in the AppIcon.appiconset entries
are missing their "filename" properties; update the Contents.json entries for
those appearances to point to the appropriate PNG files (e.g., the dark and
tinted asset filenames) or remove the appearance entries entirely if you do not
support dark/tinted variants, ensuring you modify the entries that reference
"AppIcon_Spring_IOS_1024.png" and the appearance objects (dark/tinted) in
Contents.json.

"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
Expand Down
Binary file added Projects/App/Resources/LaunchLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 24 additions & 25 deletions Projects/App/Resources/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand All @@ -12,37 +13,35 @@
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Copyright © 2026 NDGL-iOS. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
<rect key="frame" x="0.0" y="626.5" width="375" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchLogo.png" translatesAutoresizingMaskIntoConstraints="NO" id="cNo-yc-A0v">
<rect key="frame" x="160.66666666666666" y="397" width="72" height="108"/>
<color key="backgroundColor" name="green300"/>
<constraints>
<constraint firstAttribute="height" constant="108" id="5e9-NY-PMN"/>
<constraint firstAttribute="width" constant="72" id="y5A-m1-kTT"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" name="green300"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="x7j-FC-K8j"/>
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerY" secondItem="Bcu-3y-fUS" secondAttribute="centerY" id="lzf-rq-KXE"/>
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="tjh-yU-7rh"/>
Comment on lines +31 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

safeArea 기준 중앙 정렬 → 화면 기준으로 변경 권장

현재 centerX/centerY 제약이 safeArea(Bcu-3y-fUS)를 기준으로 설정되어 있습니다. Safe area는 Dynamic Island/노치(상단 ~59pt)와 홈 인디케이터(하단 ~34pt)를 제외한 영역이므로, 로고가 시각적 화면 중앙보다 약간 아래에 표시됩니다. 런치 스크린은 일반적으로 전체 화면 기준으로 중앙 정렬하는 것이 자연스럽습니다.

🛠️ 수정 제안: safeArea → 루트 뷰 기준 중앙 정렬
-    <constraint firstItem="cNo-yc-A0v" firstAttribute="centerY" secondItem="Bcu-3y-fUS" secondAttribute="centerY" id="lzf-rq-KXE"/>
-    <constraint firstItem="cNo-yc-A0v" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="tjh-yU-7rh"/>
+    <constraint firstItem="cNo-yc-A0v" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="lzf-rq-KXE"/>
+    <constraint firstItem="cNo-yc-A0v" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="tjh-yU-7rh"/>
📝 Committable suggestion

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

Suggested change
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerY" secondItem="Bcu-3y-fUS" secondAttribute="centerY" id="lzf-rq-KXE"/>
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="tjh-yU-7rh"/>
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="lzf-rq-KXE"/>
<constraint firstItem="cNo-yc-A0v" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="tjh-yU-7rh"/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/App/Resources/LaunchScreen.storyboard` around lines 31 - 32, 현재 런치
스크린의 로고 뷰(cNo-yc-A0v)를 safeArea(Bcu-3y-fUS)에 맞춰 중앙 정렬하는 제약(lzf-rq-KXE,
tjh-yU-7rh)이 설정되어 있어 화면 시각적 중앙보다 아래에 배치됩니다; 이 두 제약을 루트 뷰(또는 루트 컨테이너 뷰)의 중심을 기준으로
변경하여 centerX/centerY가 전체 화면 기준으로 정렬되도록 수정하세요 (즉, constraint id lzf-rq-KXE와
tjh-yU-7rh의 secondItem을 safeArea(Bcu-3y-fUS)에서 루트 뷰 id로 교체하거나 루트 뷰에 상대적으로 정렬되게
재생성).

</constraints>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchLogo.png" width="72" height="108"/>
<namedColor name="green300">
<color red="0.45098042488098145" green="0.81568628549575806" blue="0.54509806632995605" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>
6 changes: 6 additions & 0 deletions Projects/App/Sources/Application/AppComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ final class AppComponent: Component<EmptyDependency>, RootDependency {
)
}
}

var templateSearchUsecase: TemplatesSearchUsecaseProtocol {
shared {
TemplatesSearchUsecase(travelTemplateRepository: travelTemplateRepository)
}
}

var authRepository: AuthRepositoryInterface {
shared { makeAuthRepository() }
Expand Down
22 changes: 22 additions & 0 deletions Projects/Core/Sources/Extensions/UIKit+/UIViewController+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// UIViewController+.swift
// Core
//
// Created by 최안용 on 2/18/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import UIKit

public extension UIViewController {
func hideKeyboard() {
let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
Comment on lines +12 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

hideKeyboard() 반복 호출 시 제스처 인식기가 누적됩니다.

hideKeyboard()를 호출할 때마다 새 UITapGestureRecognizerview에 추가됩니다. viewWillAppear 등 생명주기 메서드에서 호출하거나, 뷰 컨트롤러가 push/pop을 반복하는 경우 인식기가 계속 쌓이게 됩니다. view.endEditing(true)는 멱등(idempotent)하므로 즉각적인 오동작은 없지만, 불필요한 메모리 사용과 제스처 인식기 중복 등록이 발생합니다.

🛡️ 중복 등록 방지 수정안
 func hideKeyboard() {
+    guard !(view.gestureRecognizers?.contains(where: { $0 is UITapGestureRecognizer }) ?? false) else { return }
     let tap = UITapGestureRecognizer(target: self, action: `#selector`(UIViewController.dismissKeyboard))
     tap.cancelsTouchesInView = false
     view.addGestureRecognizer(tap)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Core/Sources/Extensions/UIKit`+/UIViewController+.swift around lines
12 - 16, The hideKeyboard() extension currently adds a new
UITapGestureRecognizer every call causing duplicates; update hideKeyboard() to
first check existing view.gestureRecognizers for an installed
UITapGestureRecognizer whose target/action matches
`#selector`(UIViewController.dismissKeyboard) (or use an associated-object stored
reference) and only add a new recognizer if none exists (alternatively remove
any previous matching recognizer before adding); keep tap.cancelsTouchesInView =
false and the selector name dismissKeyboard so behavior is unchanged.


@objc
func dismissKeyboard() {
view.endEditing(true)
}
}
16 changes: 16 additions & 0 deletions Projects/Core/Sources/Utils/URLHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// URLHelper.swift
// Core
//
// Created by 최안용 on 2/20/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import UIKit

public struct URLHelper {
public static func openURL(_ urlString: String) {
guard let url = URL(string: urlString) else { return }
UIApplication.shared.open(url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public final class TravelTemplateRepository: TravelTemplateRepositoryInterface {
}
}

public func searchTemplate() async throws -> Int {
public func searchTemplate(keyword: String, page: Int?, size: Int?) async throws -> [TripInfo] {
do {
return try await service.searchTemplate()
return try await service.searchTemplate(keyword: keyword, page: page, size: size).toDomain()
} catch {
throw error.toNDGLError()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// PlaceRepositoryProtocol.swift
// PlaceRepositoryInterface.swift
// Domain
//
// Created by 최안용 on 2/14/26.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// TravelProgramRepositoryProtocol.swift
// TravelProgramRepositoryInterface.swift
// Domain
//
// Created by 최안용 on 2/14/26.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// TravelTemplateRepositoryProtocol.swift
// TravelTemplateRepositoryInterface.swift
// Domain
//
// Created by 최안용 on 2/14/26.
Expand All @@ -11,7 +11,7 @@ import Foundation
public protocol TravelTemplateRepositoryInterface {
func fetchPlaces(travelId: Int, day: Int) async throws -> [TravelPlace]
func fetchTravelDetail(id: Int) async throws -> TravelDetail
func searchTemplate() async throws -> Int
func searchTemplate(keyword: String, page: Int?, size: Int?) async throws -> [TripInfo]
func fetchPopularTripList(id: Int?, page: Int?, size: Int?) async throws -> [TripInfo]
func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// UserTravelRepositoryProtocol.swift
// UserTravelRepositoryInterface.swift
// Domain
//
// Created by 최안용 on 2/14/26.
Expand Down
37 changes: 37 additions & 0 deletions Projects/Domain/Sources/UseCase/TemplatesSearchUsecase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// TemplatesSearchUsecase.swift
// Domain
//
// Created by 최안용 on 2/19/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public protocol TemplatesSearchUsecaseProtocol {
func searchTemplate(keyword: String, page: Int?, size: Int?) async throws -> [TripInfo]
}

public extension TemplatesSearchUsecaseProtocol {
func searchTemplate(
keyword: String,
page: Int? = nil,
size: Int? = nil
) async throws -> [TripInfo] {
try await self.searchTemplate(keyword: keyword, page: page, size: size)
}
}
Comment on lines +15 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

프로토콜 요구 메서드와 동일한 기본 시그니처를 가진 extension 기본 구현 → 무한 재귀 위험

TemplatesSearchUsecaseProtocol의 extension이 프로토콜 요구사항인 searchTemplate(keyword:page:size:)와 동일한 기본 시그니처를 가지며, 그 내부에서 self.searchTemplate(keyword:page:size:)를 호출합니다.

searchTemplate(keyword:page:size:)를 직접 구현하지 않고 extension에 의존하는 타입(예: 테스트용 Mock)의 경우, searchTemplate(keyword: "test")처럼 기본값을 사용하는 호출이 extension → self.searchTemplate(keyword:page:size:) → extension 순으로 무한 재귀를 일으켜 BAD_ACCESS 크래시가 발생합니다. extension 함수 foo가 코드가 BAD_ACCESS로 크래시될 때까지 재귀적으로 자신을 호출합니다. 두 경우 모두 치명적인 런타임 문제인 무한 재귀 호출이 발생하며, 메서드는 결국 크래시로 이어지는 반복 호출을 계속합니다.

현재 TemplatesSearchUsecase는 메서드를 직접 구현하므로 프로덕션 코드는 안전하지만, 구현 없이 프로토콜만 채택하는 Mock이 추가될 때 즉시 크래시가 발생합니다.

권장 해결책은 extension 내 기본 파라미터를 가진 메서드의 이름을 바꿔 프로토콜 요구 메서드와 명확히 구분하는 것입니다. 예를 들어 foo(i1:i2:=) 대신 fooDefault(i1:i2:=)를 사용하면, extension의 기본 구현이 프로토콜 요구 메서드와 명확히 구분되어 재귀 호출 문제를 방지합니다.

🐛 무한 재귀를 방지하는 수정 제안
 public extension TemplatesSearchUsecaseProtocol {
-    func searchTemplate(
-        keyword: String,
-        page: Int? = nil,
-        size: Int? = nil
-    ) async throws -> [TripInfo] {
-        try await self.searchTemplate(keyword: keyword, page: page, size: size)
-    }
+    // 편의 메서드: page/size 없이 키워드만으로 검색 (프로토콜 요구 메서드와 다른 시그니처)
+    func searchTemplate(keyword: String) async throws -> [TripInfo] {
+        try await searchTemplate(keyword: keyword, page: nil, size: nil)
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Domain/Sources/UseCase/TemplatesSearchUsecase.swift` around lines 15
- 23, The extension on TemplatesSearchUsecaseProtocol defines a
default-signature method searchTemplate(keyword:page:size:) that simply calls
self.searchTemplate(...) which causes infinite recursion for types that don't
implement the protocol method (e.g., mocks); fix it by renaming the extension
helper to a different name (e.g., searchTemplateDefault or
searchTemplateWithDefaults) inside the TemplatesSearchUsecaseProtocol extension,
have that helper apply the defaulting logic and then call the protocol
requirement self.searchTemplate(keyword:page:size:) with explicit parameters,
and update any internal callers to use the new helper name so the protocol
requirement and the extension helper are no longer the same symbol.


public final class TemplatesSearchUsecase {
private let travelTemplateRepository: TravelTemplateRepositoryInterface

public init(travelTemplateRepository: TravelTemplateRepositoryInterface) {
self.travelTemplateRepository = travelTemplateRepository
}
}

extension TemplatesSearchUsecase: TemplatesSearchUsecaseProtocol {
public func searchTemplate(keyword: String, page: Int?, size: Int?) async throws -> [TripInfo] {
try await travelTemplateRepository.searchTemplate(keyword: keyword, page: page, size: size)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class HomeViewController: UIViewController, HomeViewControllable {
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private let networkErrorView = NDGLErrorView()

private var dataSource: UICollectionViewDiffableDataSource<HomeSectionKind, HomeItem>?
private var dataSource: UICollectionViewDiffableDataSource<HomeSectionKind, HomeItem>! = nil

// MARK: - Lifecycle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ enum HomeSectionKind: Int, CaseIterable {
var headerTitle: String {
switch self {
case .category:
"인기 여행 따라가기"
return "인기 여행 따라가기"
case .recommendedTrip:
"나혜주님께 추천하는\n따라가기 여행 콘텐츠에요!"
default: ""
let nickname = UserDefaults.standard.string(forKey: "nickname") ?? "알 수 없음"
return "\(nickname)님께 추천하는\n따라가기 여행 콘텐츠에요!"
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for "nickname" key usage across the codebase
rg -n '"nickname"' --type swift -A2 -B2

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 2041


🏁 Script executed:

#!/bin/bash
# Examine the HomeSectionKind.swift file to confirm current state
find . -name "HomeSectionKind.swift" -type f

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Look for UserDefaults patterns across the codebase
rg -n 'UserDefaults\.standard' --type swift | head -30

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1010


HomeSectionKind 열거형에서 UserDefaults를 직접 읽는 것은 아키텍처 위반입니다.

headerTitle computed property에서 UserDefaults.standard라는 전역 가변 상태에 직접 의존하고 있습니다. 이로 인해:

  • 숨겨진 의존성: 호출부는 headerTitle이 외부 상태를 읽는다는 사실을 알 수 없습니다.
  • 테스트 어려움: 이 값을 단위 테스트하려면 UserDefaults에 사전 데이터를 세팅해야 합니다.
  • 전역 상태 결합: 순수한 값 타입인 열거형이 global state에 몰래 의존합니다.

코드에 // 임시 주석이 있듯이 이 구현은 프로덕션 진입 전에 제거해야 합니다. 닉네임을 ViewModel 또는 presenter에서 주입받도록 리팩터링하세요.

♻️ 제안

headerTitle을 파라미터를 받는 메서드로 변경하거나 닉네임을 외부에서 주입받도록 수정합니다.

-    var headerTitle: String {
+    func headerTitle(nickname: String? = nil) -> String {
         switch self {
         case .category:
             return "인기 여행 따라가기"
         case .recommendedTrip:
-            let nickname = UserDefaults.standard.string(forKey: "nickname") ?? "알 수 없음"
-            return "\(nickname)님께 추천하는\n따라가기 여행 콘텐츠에요!"
+            let name = nickname ?? "알 수 없음"
+            return "\(name)님께 추천하는\n따라가기 여행 콘텐츠에요!"
         default: return ""
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Projects/Features/HomeFeature/Sources/Views/CollectionView/SectionKind/HomeSectionKind.swift`
around lines 22 - 23, The headerTitle computed property in the HomeSectionKind
enum currently reads UserDefaults.standard directly (hidden global dependency);
change it to accept the nickname from the outside (e.g., convert headerTitle
into a method like headerTitle(nickname: String) or add a function on
HomeSectionKind that takes a nickname parameter) and remove any direct
UserDefaults access from HomeSectionKind; update call sites
(ViewModel/presenter/View) to fetch the nickname from UserDefaults or its own
state and pass it into HomeSectionKind.headerTitle(...) so the enum no longer
depends on global state.

default: return ""
}
}
}
8 changes: 6 additions & 2 deletions Projects/Features/MainFeature/Sources/MainBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@
//

import Domain
import Data
import FollowFeature
import PopularTravelFeature
import SettingFeature
import SearchFeature
import SettingFeature
import TabBarFeature

import RIBs

public protocol MainDependency: Dependency {
var homeUsecase: HomeUsecaseProtocol { get }
var followDetailUsecase: FollowDetailUsecaseProtocol { get }
var templateSearchUsecase: TemplatesSearchUsecaseProtocol { get }
}

final class MainComponent: Component<MainDependency>, FollowDetailDependency, PopularTravelDependency,SearchDependency, SettingDependency, TabBarDependency {
var searchUsecase: TemplatesSearchUsecaseProtocol {
dependency.templateSearchUsecase
}

var followDetailUsecase: FollowDetailUsecaseProtocol {
dependency.followDetailUsecase
}
Expand Down
4 changes: 4 additions & 0 deletions Projects/Features/MainFeature/Sources/MainInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,8 @@ final class MainInteractor: PresentableInteractor<MainPresentable>, MainInteract
func routeToSearch() {
router?.attachSearch()
}

func attachFollowDetail(with recommendationId: Int) {
router?.attachFollow(with: recommendationId)
}
}
4 changes: 2 additions & 2 deletions Projects/Features/MainFeature/Sources/MainRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ final class MainRouter: ViewableRouter<MainInteractable, MainViewControllable>,
let router = searchBuilder.build(withListener: interactor)
self.searchRouter = router
attachChild(router)
viewController.pushViewController(router.viewControllable, animated: true)
viewController.pushViewController(router.viewControllable, animated: false)
}

func detachSearch() {
guard let router = searchRouter else { return }

if viewController.containsInStack(router.viewControllable) {
viewController.popRootViewController(animated: true)
viewController.popRootViewController(animated: false)
}

detachChild(router)
Expand Down
5 changes: 5 additions & 0 deletions Projects/Features/RootFeature/Sources/RootBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ public protocol RootDependency: Dependency {
var followDetailUsecase: FollowDetailUsecaseProtocol { get }
var authRepository: AuthRepositoryInterface { get }
var tokenRepository: TokenRepositoryProtocol { get }
var templateSearchUsecase: TemplatesSearchUsecaseProtocol { get }
}

// MARK: - RootComponent

final class RootComponent: Component<RootDependency>, MainDependency {
var templateSearchUsecase: TemplatesSearchUsecaseProtocol {
dependency.templateSearchUsecase
}

var followDetailUsecase: FollowDetailUsecaseProtocol {
dependency.followDetailUsecase
}
Expand Down
10 changes: 9 additions & 1 deletion Projects/Features/RootFeature/Sources/RootInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,22 @@ final class RootInteractor: PresentableInteractor<RootPresentable>, RootInteract
if let uuid = self.tokenRepository.get(.uuid) {
let loginResult = try await self.authRepository.login(uuid: uuid)
self.tokenRepository.save(loginResult.accessToken, for: .accessToken)

// 임시
UserDefaults.standard.set(loginResult.uuid, forKey: "uuid")
UserDefaults.standard.set(loginResult.nickname, forKey: "nickname")
} else {
let fcmToken = self.tokenRepository.get(.fcmToken) ?? UUID().uuidString
let signupResult = try await self.authRepository.signup(
info: SignupInfo(fcmToken: fcmToken)
)
self.tokenRepository.save(signupResult.uuid, for: .uuid)
self.tokenRepository.save(signupResult.accessToken, for: .accessToken)


// 임시
UserDefaults.standard.set(signupResult.uuid, forKey: "uuid")
UserDefaults.standard.set(signupResult.nickname, forKey: "nickname")
Comment on lines +73 to +87
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

UUID·닉네임을 UserDefaults에 저장하는 것은 보안 위험

uuid는 인증 자격증명으로 사용되고, nickname은 PII입니다. 두 값 모두 암호화되지 않은 UserDefaults에 저장되면 동일 컨테이너에 접근할 수 있는 환경에서 평문으로 노출됩니다.

  • uuid는 이미 tokenRepository.save(_:for:)를 통해 Keychain에 안전하게 저장되고 있습니다(line 82). SettingFeature 등 소비자 모듈이 해당 값을 필요로 한다면, UserDefaults 복사본이 아닌 tokenRepository에서 직접 읽도록 수정해 주세요.
  • nickname 역시 tokenRepository(Keychain)에 별도 TokenType을 추가하거나, 안전한 저장소를 활용하는 방향을 권장합니다.

"임시" 표시가 있더라도 이 패턴이 다른 모듈에 의해 의존될 경우 제거가 어려워질 수 있으니, 가능한 한 빠르게 올바른 경로(Keychain/TokenRepository)로 마이그레이션해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/RootFeature/Sources/RootInteractor.swift` around lines 73 -
87, Remove the insecure UserDefaults writes for uuid and nickname in
RootInteractor (the two UserDefaults.standard.set(...) lines) and instead rely
on tokenRepository for persistence: stop writing loginResult.uuid and
signupResult.uuid to UserDefaults and ensure consumers read uuid via
tokenRepository.get(.uuid) (the existing tokenRepository.save(_:for:) already
stores uuid in Keychain), and add a secure storage entry for nickname by
extending TokenType (e.g., .nickname) and using tokenRepository.save(nickname,
for: .nickname) where signup/login results are handled so nickname is stored in
Keychain rather than UserDefaults.


let loginResult = try await self.authRepository.login(uuid: signupResult.uuid)
self.tokenRepository.save(loginResult.accessToken, for: .accessToken)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// SearchResultPresentationModel.swift
// SearchFeature
//
// Created by 최안용 on 2/18/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

import Domain

struct SearchResultPresentationModel {
let resultTrip: [SearchResultPresentationModel.ResultTrip]

struct ResultTrip: Hashable {
let id: Int
let title: String
let thumbnailUrl: String
let creator: String
let schedule: String
let country: String
let city: String
}
}

extension TripInfo {
func toSearchResultModel() -> SearchResultPresentationModel.ResultTrip {
return SearchResultPresentationModel.ResultTrip(
id: self.id,
title: self.title,
thumbnailUrl: self.thumbnailUrl,
creator: self.creator,
schedule: "\(self.nights)박 \(self.days)일",
country: self.country,
city: self.city
)
}
}
Loading