Model

namespace G24W1502WPFRest;

// 서버로부터 받은 JSON 예제
//{
//    "id": 516,
//    "title": "PUBG: BATTLEGROUNDS",
//    "thumbnail": "<https://www.freetogame.com/g/516/thumbnail.jpg>",
//    "short_description": "Get into the action in one of the longest running battle royale games PUBG Battlegrounds.",
//    "game_url": "<https://www.freetogame.com/open/pubg>",
//    "genre": "Shooter",
//    "platform": "PC (Windows)",
//    "publisher": "KRAFTON, Inc.",
//    "developer": "KRAFTON, Inc.",
//    "release_date": "2022-01-12",
//    "freetogame_profile_url": "<https://www.freetogame.com/pubg>"
//},

// Kotlin의 data class에 해당
public record class Game(
    int id,
    string title,
    string thumbnail,
    string short_description,
    string game_url,
    string genre,
    string platform,
    string publisher,
    string developer,
    string release_date,
    string freetogame_profile_url
);

////이렇게 만들어도 됨
//public class Game {
//    public int id;
//    public string title { get; set; }
//    public string release_date { get; set; }
//    public string genre { get; set; }
//    public string publisher { get; set; }
//    public string developer { get; set; }
//    public string short_description { get; set; }
//    public string platform { get; set; }
//    public string thumbnail { get; set; }
//    public string game_url { get; set; }
//    public string freetogame_profile_url { get; set; }
//}

⭐ViewModel

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Windows;

namespace G24W1502WPFRest;

public class GameViewModel : INotifyPropertyChanged {
    private ObservableCollection<Game> _games
        = new ObservableCollection<Game>();

    public ObservableCollection<Game> Games => _games;

    private Game? _selectedGame = null;
    public Game? SelectedGame {
        get => _selectedGame;
        set {
            if (value == null || _selectedGame == value)
                return;

            _selectedGame = value;
            OnPropertyChanged(nameof(SelectedGame));
            OnPropertyChanged(nameof(IsSelected));
        }
    }

    public Visibility IsSelected  => (_selectedGame == null) 
            ? Visibility.Hidden 
            : Visibility.Visible;

    //---------------------------------------------

    // 참고
    // <https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient>
    // REST API 접속을 위한 변수
    private static HttpClient sharedClient = new() {
        BaseAddress = new Uri("<https://www.freetogame.com/api/>"),
    };

    public async Task GetGames() {
        try {
            // <https://www.freetogame.com/api/games?platform=pc> 에서 JSON 받아옴
            HttpResponseMessage response
                    = await sharedClient.GetAsync("games?platform=pc");
            response.EnsureSuccessStatusCode();

            string result = await response.Content.ReadAsStringAsync();
            // 디버깅을 위해 출력 창에 결과 출력
            //System.Diagnostics.Debug.WriteLine(result);
            if (string.IsNullOrEmpty(result)) return;

            // 서버에서 받은 JSON을 List로 변환
            List<Game>? resultGames
                = JsonSerializer.Deserialize<List<Game>>(result);
            if (resultGames == null) return;

            // ObservableCollection에 추가
            foreach (Game game in resultGames) {
                _games.Add(game);
            }
        }
        catch (HttpRequestException e) {
            // HTTP 요청 관련 예외 처리
            System.Diagnostics.Debug.WriteLine($"Request error: {e.Message}");
        }
        catch (JsonException e) {
            // JSON 파싱 관련 예외 처리
            System.Diagnostics.Debug.WriteLine($"JSON parse error: {e.Message}");
        }
        catch (Exception e) {
            // 그 외의 예외 처리
            System.Diagnostics.Debug.WriteLine($"Unexpected error: {e.Message}");
        }
    }

    //---------------------------------------------
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propName = "") {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}

View

<Window x:Class="G24W1502WPFRest.MainWindow"
        xmlns="<http://schemas.microsoft.com/winfx/2006/xaml/presentation>"
        xmlns:x="<http://schemas.microsoft.com/winfx/2006/xaml>"
        xmlns:d="<http://schemas.microsoft.com/expression/blend/2008>"
        xmlns:mc="<http://schemas.openxmlformats.org/markup-compatibility/2006>"
        xmlns:local="clr-namespace:G24W1502WPFRest"
        mc:Ignorable="d"
        Title="무료 게임 리스트" Height="450" Width="600">
    <Border Padding="8">
        <DockPanel>
            <DataGrid
                x:Name="GameGrid"
                DockPanel.Dock="Left"
                AutoGenerateColumns="False"
                Width="Auto"
                SelectionMode="Single"
                ItemsSource="{ Binding Games }"
                SelectedItem="{ Binding SelectedGame }">
                <DataGrid.Columns>
                    <DataGridTemplateColumn 
                        Header="이미지" 
                        Width="100">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Image 
                                    Source="{ Binding thumbnail }"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn 
                        Header="게임 제목"
                        Width="150">    
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock 
                                    Text="{ Binding title }"
                                    Padding="8 0"
                                    VerticalAlignment="Center"
                                    TextWrapping="Wrap"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
            <Border 
                Padding="8" 
                Background="LightGray">
                <StackPanel
                    Visibility="{ Binding IsSelected }">
                    <Image 
                        Source="{ Binding SelectedGame.thumbnail }"/>
                    <TextBlock 
                        Text="{ Binding SelectedGame.title}"
                        FontSize="30" FontWeight="Bold"
                        Background="DarkBlue" Foreground="White"
                        Padding="8"
                        TextWrapping="Wrap"
                        TextAlignment="Center"/>
                    <TextBlock 
                        Text="{ Binding SelectedGame.short_description}"
                        Margin="0 8"
                        TextWrapping="Wrap"
                        TextAlignment="Justify"/>   
                    <StackPanel 
                        Orientation="Horizontal"
                        Margin="0 8">
                        <TextBlock
                            Text="장르: "
                            FontSize="16"/>
                        <TextBlock 
                            Text="{ Binding SelectedGame.genre}"
                            FontSize="16"/>
                    </StackPanel>
                    <UniformGrid
                        Rows="1">
                        <Button
                            Content="게임 프로필"
                            Padding="8"
                            Margin="0 8 4 8"
                            Click="StartWebBrowser"
                            Tag="{ Binding SelectedGame.freetogame_profile_url}"/>
                        <Button
                            Content="게임 홈페이지"
                            Padding="8"
                            Margin="4 8 0 8"
                            Click="StartWebBrowser"
                            Tag="{ Binding SelectedGame.game_url}"/>
                    </UniformGrid>
                </StackPanel>
            </Border>
        </DockPanel>
    </Border>
</Window>

xaml.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;

namespace G24W1502WPFRest;
public partial class MainWindow : Window
{
    private GameViewModel vm = new GameViewModel();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = vm;

        //GameGrid.ItemsSource = vm.Games;

        InitializeGames();
    }

    private async void InitializeGames() {
        await vm.GetGames();
    }

    public void StartWebBrowser(object sender, RoutedEventArgs e) {
        string url = (string)((Button)sender).Tag;
        Process.Start(
            new ProcessStartInfo("cmd", $"/c start {url}") {
                CreateNoWindow = true
            }
        );
    }
}

여기에 Layout 추가

StackPanel

<StackPanel Orientation="Vertical" Margin="10"> //Horizontal 왼쪽에서 오른쪽
        <Button Content="버튼 1" Margin="5"/>   //Vertical 위에서 아래
        <Button Content="버튼 2" Margin="5"/>
        <Button Content="버튼 3" Margin="5"/>
</StackPanel>

TextBlock

  <TextBlock Text="안녕하세요, WPF!"
             FontSize="24"
             FontWeight="Bold"
             Foreground="DarkBlue"
             TextAlignment="Center"
             Margin="10"/>

UniformGrid

 <UniformGrid Rows="2" Columns="3" Margin="10"> // 2행 3열로 만들어짐
        <Button Content="1"/>
        <Button Content="2"/>
        <Button Content="3"/>
        <Button Content="4"/>
        <Button Content="5"/>
        <Button Content="6"/>
    </UniformGrid>

Border