diff --git a/docs/rp_redirection_implementation.md b/docs/rp_redirection_implementation.md new file mode 100644 index 00000000..d7ee2202 --- /dev/null +++ b/docs/rp_redirection_implementation.md @@ -0,0 +1,138 @@ +# 연동된 RP 홈페이지 이동 기능 구현 가이드 + +## 1. 개요 +UserFront 대시보드의 '활동상황' 섹션에서 연동된 RP(Relying Party) 카드를 클릭했을 때, 해당 서비스의 홈페이지로 이동하는 기능의 구현 상세입니다. +특히, RP 설정에 홈페이지 주소(`client_uri`)가 명시되지 않은 경우에도 `Redirect URI`를 기반으로 주소를 추론하여 이동할 수 있도록 **Fallback 로직**이 적용되었습니다. + +## 2. 동작 흐름 (Data Flow) + +1. **초기 로딩**: 사용자가 대시보드에 접속하면 프론트엔드는 백엔드에 연동된 RP 목록을 요청합니다. +2. **데이터 가공 (Backend)**: + * 백엔드는 Ory Hydra에서 사용자의 동의(Consent) 세션을 조회합니다. + * 각 RP(Client) 정보에서 `client_uri`를 확인합니다. + * **Fallback**: 만약 `client_uri`가 비어있다면, `redirect_uris`의 첫 번째 주소를 파싱하여 `Scheme`과 `Host` (예: `https://gitea.hmac.kr`)를 추출해 홈페이지 주소로 사용합니다. +3. **렌더링 (Frontend)**: + * 응답받은 목록을 기반으로 카드를 생성합니다. + * RP 상태가 '활성(active)'인 경우에만 클릭 이벤트를 활성화합니다. +4. **사용자 인터랙션**: + * 사용자가 카드를 클릭하면 `url_launcher`를 통해 새 브라우저 탭에서 해당 주소를 엽니다. + * 주소가 없는 경우 사용자에게 안내 메시지(SnackBar)를 표시합니다. + +--- + +## 3. 백엔드 구현 상세 (Go) + +### 파일: `backend/internal/handler/auth_handler.go` + +#### 3.1 구조체 변경 +API 응답 모델인 `linkedRpSummary`에 `URL` 필드를 추가하여 프론트엔드로 전달할 수 있게 했습니다. + +```go +type linkedRpSummary struct { + ID string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo,omitempty"` + URL string `json:"url,omitempty"` // 추가된 필드 + LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"` + Status string `json:"status"` + Scopes []string `json:"scopes,omitempty"` +} +``` + +#### 3.2 URL 할당 및 Fallback 로직 (`ListLinkedRps`) +Hydra Client 정보 매핑 시, `ClientURI` 부재 시 `RedirectURIs`를 활용하는 로직이 핵심입니다. + +```go +// ClientURI가 없으면 RedirectURIs에서 호스트 부분만 추출하여 URL로 사용 (Fallback) +clientURL := strings.TrimSpace(client.ClientURI) +if clientURL == "" && len(client.RedirectURIs) > 0 { + // 예: https://gitea.hmac.kr/callback -> https://gitea.hmac.kr + if parsed, err := url.Parse(client.RedirectURIs[0]); err == nil { + clientURL = fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) + } +} + +// ... +records[clientID] = &linkedRpRecord{ + linkedRpSummary: linkedRpSummary{ + // ... + URL: clientURL, // 가공된 URL 할당 + }, +} +``` + +--- + +## 4. 프론트엔드 구현 상세 (Flutter) + +### 파일: `userfront/lib/features/dashboard/presentation/dashboard_screen.dart` + +#### 4.1 모델 업데이트 +백엔드 응답을 처리하기 위해 `LinkedRp` 모델에 `url` 필드를 추가했습니다. + +```dart +class LinkedRp { + final String id; + // ... + final String url; // 추가된 필드 + + factory LinkedRp.fromJson(Map json) { + return LinkedRp( + // ... + url: json['url']?.toString() ?? '', + ); + } +} +``` + +#### 4.2 UI 인터랙션 구현 (`_buildActivityCard`) +카드의 클릭 가능 여부를 판단하고, 클릭 시 이동 로직을 처리합니다. + +1. **클릭 조건**: RP 상태가 '활성'(`isActive`)이면 클릭 가능하도록 설정합니다. +2. **시각적 피드백**: + * 활성 상태인 경우 테두리 색상(Green)과 그림자 효과(BoxShadow)를 적용하여 클릭 가능함을 암시합니다. + * `MouseRegion`을 사용하여 마우스 오버 시 포인터 커서(`SystemMouseCursors.click`)를 표시합니다. +3. **이동 로직**: + * `url_launcher` 패키지의 `launchUrl`을 사용합니다. + * URL이 비어있거나 유효하지 않은 경우 `ScaffoldMessenger`를 통해 안내 메시지를 띄웁니다. + +```dart + // 활성 상태면 클릭 가능 + final isClickable = isActive; + + // ... (UI 스타일링 코드 생략) + + if (isClickable) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () async { + if (item.url != null && item.url!.isNotEmpty) { + final uri = Uri.parse(item.url!); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + // 브라우저 실행 실패 시 + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('해당 링크를 열 수 없습니다.')), + ); + } + } + } else { + // URL 정보가 없는 경우 (백엔드 Fallback 실패 등) + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('이동할 페이지 주소(Client URI)가 설정되지 않았습니다.')), + ); + } + } + }, + child: opaqueCard, + ), + ); + } +``` + +## 5. 요약 +이 기능은 사용자가 별도의 설정 없이도 연동된 서비스로 쉽게 이동할 수 있도록 편의성을 제공합니다. 백엔드에서의 **지능적인 주소 추론**과 프론트엔드에서의 **직관적인 UI 피드백**이 결합되어 완성되었습니다.