diff --git a/app/(tabs)/(home)/add-link.tsx b/app/(tabs)/(home)/add-link.tsx
index 6eb4ee0..2c313ea 100644
--- a/app/(tabs)/(home)/add-link.tsx
+++ b/app/(tabs)/(home)/add-link.tsx
@@ -1,8 +1,6 @@
import { useState } from 'react';
import {
- ActivityIndicator,
KeyboardAvoidingView,
- Linking,
Platform,
StyleSheet,
Text,
@@ -15,14 +13,35 @@ import { Stack, router } from 'expo-router';
import { ScanButton } from '@/components/ui/scan-button';
import { Colors, Typography } from '@/constants/theme';
+function getRawHostname(value: string): string | null {
+ const authority = value.match(/^https?:\/\/([^/?#]+)/i)?.[1];
+ if (!authority) return null;
+
+ return authority.split('@').pop()?.split(':')[0] ?? null;
+}
+
+function isValidHostname(hostname: string): boolean {
+ const labels = hostname.split('.');
+ if (labels.length < 2) return false;
+ if (labels.some((label) => !label)) return false;
+
+ const tld = labels[labels.length - 1];
+ if (!/^[a-z]{2,}$/i.test(tld)) return false;
+
+ return labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label));
+}
+
function isValidUrlFormat(value: string): boolean {
const trimmed = value.trim();
- if (!/^https?:\/\//i.test(trimmed)) return false;
+ if (!trimmed || /\s/.test(trimmed)) return false;
+
+ const candidate = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
+ const rawHostname = getRawHostname(candidate);
+ if (!rawHostname || !isValidHostname(rawHostname)) return false;
+
try {
- const parsed = new URL(trimmed);
- const parts = parsed.hostname.split('.');
- const tld = parts[parts.length - 1];
- return parts.length >= 2 && tld.length >= 2;
+ const parsed = new URL(candidate);
+ return isValidHostname(parsed.hostname);
} catch {
return false;
}
@@ -31,9 +50,8 @@ function isValidUrlFormat(value: string): boolean {
export default function AddLinkScreen() {
const [url, setUrl] = useState('');
const [error, setError] = useState('');
- const [isChecking, setIsChecking] = useState(false);
- const handleScan = async () => {
+ const handleScan = () => {
const trimmed = url.trim();
if (!trimmed) {
@@ -41,27 +59,13 @@ export default function AddLinkScreen() {
return;
}
- // 1단계: new URL()로 형식 검증
if (!isValidUrlFormat(trimmed)) {
setError('올바르지 않은 URL 입력입니다.');
return;
}
- // 2단계: Linking.canOpenURL()로 실제 열기 가능 여부 확인
- setIsChecking(true);
- try {
- const canOpen = await Linking.canOpenURL(trimmed);
- if (!canOpen) {
- setError('올바르지 않은 URL 입력입니다.');
- return;
- }
- setError('');
- router.push({ pathname: '/(tabs)/(home)/scanning', params: { url: trimmed } });
- } catch {
- setError('URL 확인 중 오류가 발생했습니다.');
- } finally {
- setIsChecking(false);
- }
+ setError('');
+ router.push({ pathname: '/(tabs)/(home)/scanning', params: { url: trimmed } });
};
const handleChangeUrl = (value: string) => {
@@ -70,7 +74,7 @@ export default function AddLinkScreen() {
};
const hasError = error.length > 0;
- const scanDisabled = !url.trim() || isChecking;
+ const scanDisabled = !url.trim();
return (
<>
@@ -118,9 +122,8 @@ export default function AddLinkScreen() {
keyboardType="url"
returnKeyType="search"
onSubmitEditing={handleScan}
- editable={!isChecking}
/>
- {url.length > 0 && !isChecking && (
+ {url.length > 0 && (
{ setUrl(''); setError(''); }}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
@@ -130,13 +133,7 @@ export default function AddLinkScreen() {
)}
- {isChecking ? (
-
-
-
- ) : (
-
- )}
+
{/* 에러 메시지 */}
@@ -232,17 +229,6 @@ const styles = StyleSheet.create({
color: Colors.brand.primary,
lineHeight: 16,
},
- loadingBox: {
- width: 60,
- height: 60,
- borderRadius: 16,
- borderWidth: 1.5,
- borderColor: Colors.brand.line,
- backgroundColor: Colors.brand.background,
- alignItems: 'center',
- justifyContent: 'center',
- },
-
// 에러
errorText: {
...Typography.body,
diff --git a/components/ui/card-link.tsx b/components/ui/card-link.tsx
index 6bef5a1..bf6f71b 100644
--- a/components/ui/card-link.tsx
+++ b/components/ui/card-link.tsx
@@ -30,13 +30,6 @@ const VERDICT_LABELS: Record = {
const VERDICT_COLORS: Record = Colors.brand.verdict;
-function normalizeLinkUrl(value?: string | null) {
- const trimmed = value?.trim();
- if (!trimmed) return null;
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed;
- return `https://${trimmed}`;
-}
-
function getFirstText(...values: (string | null | undefined)[]) {
return values.find((value) => value?.trim())?.trim();
}
@@ -58,7 +51,7 @@ export function CardLink({
const moreRef = useRef(null);
const displayUrl = getFirstText(finalUrl, originalUrl) ?? 'URL 정보 없음';
const displayTitle = getFirstText(title, summary) ?? '제목 없음';
- const openUrl = normalizeLinkUrl(getFirstText(finalUrl, originalUrl));
+ const openUrl = getFirstText(finalUrl, originalUrl);
const normalizedVerdict = verdict && verdict in VERDICT_LABELS ? verdict : undefined;
const statusLabel = normalizedVerdict ? VERDICT_LABELS[normalizedVerdict] : (getFirstText(label) ?? '결과 없음');
const statusColors = normalizedVerdict ? VERDICT_COLORS[normalizedVerdict] : undefined;
@@ -88,12 +81,6 @@ export function CardLink({
}
try {
- const canOpen = await Linking.canOpenURL(openUrl);
- if (!canOpen) {
- Alert.alert('URL을 열 수 없어요', '외부 브라우저에서 열 수 없는 주소입니다.');
- return;
- }
-
await Linking.openURL(openUrl);
} catch {
Alert.alert('URL을 열 수 없어요', '잠시 후 다시 시도해주세요.');