# 연동된 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 피드백**이 결합되어 완성되었습니다.