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