Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
- When a feature depends on native capabilities such as camera, location, media library, sharing, secure storage, or app permissions, prioritize iOS/Android behavior and native Expo APIs.
- Do not block mobile feature work just because the same flow cannot fully work on web. Provide a minimal web fallback only when it is needed for local development, type checking, preview safety, or export stability.

## Mandatory Simulator Testing

- Never use Expo web, a deployed web app, or a browser to test or validate Soundlog product behavior, UI, server integration, permissions, or regressions.
- Run every product test and visual verification in the iOS Simulator. A browser result must never be treated as evidence that the app works correctly.
- After a Soundlog app change, launch the app in the iOS Simulator and verify the affected flow there before reporting completion.
- API health checks and command-line diagnostics may support investigation, but they do not replace simulator verification and must not be reported as completed app testing.
- Use a web target only for explicit web build or export compatibility work. Even then, do not use it for product acceptance testing unless the user's latest message explicitly overrides this rule.

## Recap And Log Domain

- Before changing Recap, Log, camera capture, travel mode, route tracking, map pins, visibility, or related API behavior, read `docs/product/RECAP_LOG_DOMAIN_MODEL.md`.
Expand Down
48 changes: 18 additions & 30 deletions app/(tabs)/music.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { CurrentSoundtrackCard } from "@/components/home/CurrentSoundtrackCard";
import { HomeSoundtrackBottomSheet } from "@/components/home/HomeSoundtrackBottomSheet";
import { HomeHeader } from "@/components/home/HomeHeader";
import { LocationContextCard } from "@/components/home/LocationContextCard";
import { ManualPlacePickerModal } from "@/components/home/ManualPlacePickerModal";
import {
MoodRecommendationSection,
isMoodRecommendationFilter,
Expand Down Expand Up @@ -142,7 +141,6 @@ function HomeContent() {
const insets = useSafeAreaInsets();
const authStatus = useAuthStore((state) => state.status);
const [actionMessage, setActionMessage] = useState<string>();
const [isPlacePickerVisible, setIsPlacePickerVisible] = useState(false);
const [isSoundtrackSheetVisible, setIsSoundtrackSheetVisible] =
useState(false);
const [selectedMusicPlaylistId, setSelectedMusicPlaylistId] =
Expand Down Expand Up @@ -306,18 +304,13 @@ function HomeContent() {
recommendedPlaylist ? toFeaturedPlaylist(recommendedPlaylist) : undefined,
[recommendedPlaylist],
);
const displayedFeaturedPlaylists = useMemo(() => {
if (!currentSoundtrackPlaylist) {
return featuredPlaylistsQuery.data;
}

return [
currentSoundtrackPlaylist,
...(featuredPlaylistsQuery.data ?? []).filter(
(playlist) => playlist.id !== currentSoundtrackPlaylist.id,
const displayedFeaturedPlaylists = useMemo(
() =>
featuredPlaylistsQuery.data?.filter(
(playlist) => playlist.id !== recommendedPlaylist?.id,
),
];
}, [currentSoundtrackPlaylist, featuredPlaylistsQuery.data]);
[featuredPlaylistsQuery.data, recommendedPlaylist?.id],
);
const currentSoundtrackSummary = useMemo(
() =>
recommendedPlaylist
Expand Down Expand Up @@ -420,6 +413,12 @@ function HomeContent() {
shouldReverseGeocode,
]);

useEffect(() => {
if (!currentLocation && currentPlace) {
setPlace(undefined);
}
}, [currentLocation, currentPlace, setPlace]);

useEffect(() => {
if (!recommendedPlaylist) {
return;
Expand Down Expand Up @@ -587,17 +586,6 @@ function HomeContent() {
setLocationStatus,
setPlace,
]);
const handleSelectManualPlace = useCallback(
(place: PlaceContext) => {
clearLocation();
setPlace(place);
setIsPlacePickerVisible(false);
setActionMessage(
`${place.title} 기준으로 오늘의 사운드트랙을 준비할게요.`,
);
},
[clearLocation, setPlace],
);
const handleSetCurrentLocation = useCallback(async () => {
if (!profile.locationRecommendationEnabled) {
const didEnable = await handleEnableLocationRecommendation();
Expand Down Expand Up @@ -971,7 +959,6 @@ function HomeContent() {
location={currentLocation}
onEnable={handleSetCurrentLocation}
onRefresh={handleRefreshLocation}
onSelectPlace={() => setIsPlacePickerVisible(true)}
place={currentPlace}
placeCount={nearbyPlacesQuery.data?.length ?? 0}
placeInfoMessage={placeInfoMessage}
Expand Down Expand Up @@ -1038,6 +1025,12 @@ function HomeContent() {
{actionMessage}
</AppText>
) : null}

{currentPlace?.attribution ? (
<AppText className="text-center text-[11px] leading-4 text-white/30">
{currentPlace.attribution}
</AppText>
) : null}
</ScrollView>
<HomeSoundtrackBottomSheet
actionMessage={isSoundtrackSheetVisible ? actionMessage : undefined}
Expand Down Expand Up @@ -1076,11 +1069,6 @@ function HomeContent() {
savedTrackIds={selectedMusicPlaylistSavedTrackIds}
visible={isMusicPlaylistSheetVisible}
/>
<ManualPlacePickerModal
onClose={() => setIsPlacePickerVisible(false)}
onSelect={handleSelectManualPlace}
visible={isPlacePickerVisible}
/>
{currentTrack ? <MiniPlayer /> : null}
</Screen>
);
Expand Down
3 changes: 3 additions & 0 deletions app/(tabs)/my.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppText } from '@/components/AppText';
import { AuthAccountCard } from '@/components/my/AuthAccountCard';
import { MySettingsRow } from '@/components/my/MySettingsRow';
import { PermissionSettingsCard } from '@/components/my/PermissionSettingsCard';
import { RecapLogGuide } from '@/components/my/RecapLogGuide';
import { PageHeader } from '@/components/PageHeader';
import { Screen } from '@/components/Screen';
import { SectionTitle } from '@/components/SectionTitle';
Expand Down Expand Up @@ -126,6 +127,8 @@ export default function MyScreen() {

<AuthAccountCard />

<RecapLogGuide />

<View className="mt-7">
<SectionTitle title="Soundlog 설정" />
<MySettingsRow
Expand Down
6 changes: 5 additions & 1 deletion eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
}
},
"submit": {
"production": {}
"production": {
"ios": {
"ascAppId": "6797038341"
}
}
}
}
104 changes: 76 additions & 28 deletions src/components/home/CurrentSoundtrackCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { View } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { Pressable, View } from 'react-native';

import { AppText } from '@/components/AppText';
import { IconButton } from '@/components/IconButton';
import { SectionTitle } from '@/components/SectionTitle';
import { SettingsRow } from '@/components/SettingsRow';
import type { FeaturedPlaylist, PlaceContext } from '@/types/domain';
import { getPlaceDisplayTitle } from '@/utils/placeLabel';

Expand Down Expand Up @@ -57,7 +58,7 @@ export function CurrentSoundtrackCard({
const playlistTitle =
recommendationSource === 'seed-fallback' && currentPlace
? `${placeTitle} 사운드트랙`
: playlist?.regionName ?? `${placeLabel} 사운드트랙`;
: (playlist?.regionName ?? `${placeLabel} 사운드트랙`);
const playlistDescription =
playlist?.description ??
(needsLocation
Expand Down Expand Up @@ -89,38 +90,85 @@ export function CurrentSoundtrackCard({
};

return (
<View>
<View className="gap-4">
<SectionTitle
rightContent={
sectionStatus ? (
<AppText className="text-xs font-semibold text-white/42">
{sectionStatus}
</AppText>
) : undefined
<View className="flex-row items-center gap-2">
{sectionStatus ? (
<AppText className="text-xs font-semibold text-white/42">
{sectionStatus}
</AppText>
) : null}
<IconButton
disabled={isLoading}
label={
isLoading
? '사운드트랙 추천 준비 중'
: '사운드트랙 추천 다시 받기'
}
name="refresh-cw"
onPress={onRetry}
/>
</View>
}
title="오늘의 사운드트랙"
/>
<SettingsRow
description={playlistDescription}

<Pressable
accessibilityHint={
playlist ? '추천된 곡 목록을 엽니다' : '추천을 다시 요청합니다'
}
accessibilityLabel={`${playlistTitle}, ${trackMeta}`}
accessibilityRole="button"
accessibilityState={{ disabled: isLoading || isOpeningPlaylist }}
className="min-h-[228px] overflow-hidden rounded-[28px] border border-white/10 bg-soundlog-card p-6"
disabled={isLoading || isOpeningPlaylist}
icon="disc"
label={playlistTitle}
onPress={handleOpenPlaylist}
rightText={isOpeningPlaylist ? '여는 중' : trackMeta}
/>
<SettingsRow
description={isError ? '추천을 다시 받아볼 수 있어요.' : placeCaption}
icon="map-pin"
label={placeTitle}
rightText={moodLabel}
/>
<SettingsRow
disabled={isLoading}
icon="refresh-cw"
label="추천 다시 받기"
onPress={onRetry}
rightText={isLoading ? '준비 중' : undefined}
/>
style={({ pressed }) => ({
opacity: isLoading || isOpeningPlaylist ? 0.52 : pressed ? 0.72 : 1,
})}
>
<View className="absolute -right-8 -top-10 h-40 w-40 rounded-full bg-soundlog-lime/10" />
<View className="absolute -bottom-16 -left-8 h-44 w-44 rounded-full bg-white/[0.03]" />

<View className="flex-row items-start justify-between gap-4">
<View className="h-14 w-14 items-center justify-center rounded-full bg-soundlog-lime/15">
<Feather color="#D4FF3F" name="disc" size={28} />
</View>
<View className="rounded-full bg-white/10 px-3 py-2">
<AppText className="text-xs font-semibold text-white/66">
{isOpeningPlaylist ? '여는 중' : trackMeta}
</AppText>
</View>
</View>

<View className="mt-auto pt-6">
<AppText
className="text-[28px] font-semibold leading-9 text-white"
numberOfLines={2}
>
{playlistTitle}
</AppText>
<AppText
className="mt-2 text-sm leading-6 text-white/58"
numberOfLines={2}
>
{isError ? '추천을 다시 받아볼 수 있어요.' : playlistDescription}
</AppText>
<View className="mt-5 flex-row items-center gap-2">
<Feather color="rgba(255,255,255,0.5)" name="map-pin" size={16} />
<AppText
className="min-w-0 flex-1 text-sm text-white/66"
numberOfLines={2}
>
{placeTitle} · {placeCaption}
</AppText>
<AppText className="shrink-0 text-sm font-semibold text-soundlog-lime">
{moodLabel}
</AppText>
</View>
</View>
</Pressable>
</View>
);
}
37 changes: 8 additions & 29 deletions src/components/home/LocationContextCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type LocationContextCardProps = {
onDismiss?: () => void;
onEnable: () => void;
onRefresh: () => void;
onSelectPlace?: () => void;
place?: PlaceContext;
placeCount?: number;
placeInfoMessage?: string;
Expand Down Expand Up @@ -60,7 +59,6 @@ export function LocationContextCard({
onDismiss,
onEnable,
onRefresh,
onSelectPlace,
place,
placeCount = 0,
placeInfoMessage,
Expand Down Expand Up @@ -92,8 +90,6 @@ export function LocationContextCard({
? '주변 관광지를 확인 중이에요'
: placeCount > 0
? `주변 장소 ${placeCount}곳 반영`
: place && !location
? '직접 선택한 장소로 추천 중'
: location
? updatedAt
? `${formatRecapRecordedAt(updatedAt)} 갱신`
Expand All @@ -105,14 +101,14 @@ export function LocationContextCard({
<SectionTitle
rightContent={
onDismiss ? (
<Pressable
accessibilityLabel="추천 장소 정보 닫기"
accessibilityRole="button"
className="h-11 w-11 items-center justify-center"
onPress={onDismiss}
>
<Feather color="rgba(255,255,255,0.72)" name="x" size={18} />
</Pressable>
<Pressable
accessibilityLabel="추천 장소 정보 닫기"
accessibilityRole="button"
className="h-11 w-11 items-center justify-center"
onPress={onDismiss}
>
<Feather color="rgba(255,255,255,0.72)" name="x" size={18} />
</Pressable>
) : undefined
}
title="장소 기반 추천"
Expand All @@ -128,23 +124,6 @@ export function LocationContextCard({
onPress={enabled ? onRefresh : onEnable}
rightText={isLoading ? '확인 중' : buttonLabel}
/>
{place?.attribution ? (
<SettingsRow
description={place.attribution}
icon="info"
label="위치 정보 출처"
rightText="OpenStreetMap"
/>
) : null}
{onSelectPlace ? (
<SettingsRow
description="검색한 장소를 추천 기준으로 사용해요."
disabled={isLoading}
icon="search"
label="추천 장소 직접 선택"
onPress={onSelectPlace}
/>
) : null}
</View>
);
}
Loading
Loading