1
0
forked from baron/baron-sso

RP 이동 및 연동 해지 기능 로직 문서화

This commit is contained in:
2026-02-04 11:21:18 +09:00
parent 3de77ef681
commit 336eabf492

View File

@@ -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<String, dynamic> 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 피드백**이 결합되어 완성되었습니다.