forked from baron/baron-sso
5.6 KiB
5.6 KiB
연동된 RP 홈페이지 이동 기능 구현 가이드
1. 개요
UserFront 대시보드의 '활동상황' 섹션에서 연동된 RP(Relying Party) 카드를 클릭했을 때, 해당 서비스의 홈페이지로 이동하는 기능의 구현 상세입니다.
특히, RP 설정에 홈페이지 주소(client_uri)가 명시되지 않은 경우에도 Redirect URI를 기반으로 주소를 추론하여 이동할 수 있도록 Fallback 로직이 적용되었습니다.
2. 동작 흐름 (Data Flow)
- 초기 로딩: 사용자가 대시보드에 접속하면 프론트엔드는 백엔드에 연동된 RP 목록을 요청합니다.
- 데이터 가공 (Backend):
- 백엔드는 Ory Hydra에서 사용자의 동의(Consent) 세션을 조회합니다.
- 각 RP(Client) 정보에서
client_uri를 확인합니다. - Fallback: 만약
client_uri가 비어있다면,redirect_uris의 첫 번째 주소를 파싱하여Scheme과Host(예:https://gitea.hmac.kr)를 추출해 홈페이지 주소로 사용합니다.
- 렌더링 (Frontend):
- 응답받은 목록을 기반으로 카드를 생성합니다.
- RP 상태가 '활성(active)'인 경우에만 클릭 이벤트를 활성화합니다.
- 사용자 인터랙션:
- 사용자가 카드를 클릭하면
url_launcher를 통해 새 브라우저 탭에서 해당 주소를 엽니다. - 주소가 없는 경우 사용자에게 안내 메시지(SnackBar)를 표시합니다.
- 사용자가 카드를 클릭하면
3. 백엔드 구현 상세 (Go)
파일: backend/internal/handler/auth_handler.go
3.1 구조체 변경
API 응답 모델인 linkedRpSummary에 URL 필드를 추가하여 프론트엔드로 전달할 수 있게 했습니다.
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를 활용하는 로직이 핵심입니다.
// 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 필드를 추가했습니다.
class LinkedRp {
final String id;
// ...
final String url; // 추가된 필드
factory LinkedRp.fromJson(Map<String, dynamic> json) {
return LinkedRp(
// ...
url: json['url']?.toString() ?? '',
);
}
}
4.2 UI 인터랙션 구현 (_buildActivityCard)
카드의 클릭 가능 여부를 판단하고, 클릭 시 이동 로직을 처리합니다.
- 클릭 조건: RP 상태가 '활성'(
isActive)이면 클릭 가능하도록 설정합니다. - 시각적 피드백:
- 활성 상태인 경우 테두리 색상(Green)과 그림자 효과(BoxShadow)를 적용하여 클릭 가능함을 암시합니다.
MouseRegion을 사용하여 마우스 오버 시 포인터 커서(SystemMouseCursors.click)를 표시합니다.
- 이동 로직:
url_launcher패키지의launchUrl을 사용합니다.- URL이 비어있거나 유효하지 않은 경우
ScaffoldMessenger를 통해 안내 메시지를 띄웁니다.
// 활성 상태면 클릭 가능
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 피드백이 결합되어 완성되었습니다.